From fed542ba3703777dab4bbd0971d8251cad7340e7 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Sat, 5 Nov 2022 10:41:20 +0100 Subject: [PATCH] More adds and fixes --- .../fablabchemnitz/animate_order/meta.json | 6 +- .../cutting_optimizer/cutting_optimizer.py | 3 +- .../cutting_optimizer/meta.json | 4 +- .../export_selection_as.py | 10 +- .../export_selection_as/meta.json | 4 +- .../gcode_import/gcode_import.py | 673 +++++++++++ .../gcode_import/gcode_import_gcode.inx | 54 + .../gcode_import/gcode_import_nc.inx | 54 + .../fablabchemnitz/gcode_import/meta.json | 21 + .../guilloche_creations/meta.json | 4 +- .../inventory_sticker/inventory_sticker.inx | 62 + .../inventory_sticker/inventory_sticker.py | 652 +++++++++++ .../inventory_sticker/meta.json | 20 + .../lasercut_jigsaw/lasercut_jigsaw.inx | 79 ++ .../lasercut_jigsaw/lasercut_jigsaw.py | 507 ++++++++ .../fablabchemnitz/lasercut_jigsaw/meta.json | 25 + .../fablabchemnitz/line_animator/meta.json | 4 +- extensions/fablabchemnitz/paperfold/meta.json | 21 + .../fablabchemnitz/paperfold/paperfold.inx | 108 ++ .../fablabchemnitz/paperfold/paperfold.py | 1021 +++++++++++++++++ .../fablabchemnitz/styles_to_layers/meta.json | 23 + .../styles_to_layers/styles_to_layers.inx | 67 ++ .../styles_to_layers/styles_to_layers.py | 308 +++++ extensions/fablabchemnitz/sudoku/meta.json | 21 + extensions/fablabchemnitz/sudoku/qqwing | 210 ++++ extensions/fablabchemnitz/sudoku/qqwing.exe | Bin 0 -> 135168 bytes extensions/fablabchemnitz/sudoku/sudoku.inx | 47 + extensions/fablabchemnitz/sudoku/sudoku.py | 119 ++ extensions/fablabchemnitz/visicut/meta.json | 4 +- .../fablabchemnitz/vpypetools/meta.json | 20 + .../fablabchemnitz/vpypetools/vpype_logo.svg | 392 +++++++ .../fablabchemnitz/vpypetools/vpypetools.py | 453 ++++++++ .../vpypetools/vpypetools_deduplicate.inx | 78 ++ .../vpypetools/vpypetools_filter.inx | 83 ++ .../vpypetools/vpypetools_freemode.inx | 113 ++ .../vpypetools/vpypetools_linemerge.inx | 78 ++ .../vpypetools/vpypetools_linesort.inx | 77 ++ .../vpypetools/vpypetools_multipass.inx | 77 ++ .../vpypetools/vpypetools_occult.inx | 78 ++ .../vpypetools/vpypetools_relooping.inx | 77 ++ .../vpypetools/vpypetools_splitall.inx | 76 ++ .../vpypetools/vpypetools_trim.inx | 78 ++ 42 files changed, 5790 insertions(+), 21 deletions(-) create mode 100644 extensions/fablabchemnitz/gcode_import/gcode_import.py create mode 100644 extensions/fablabchemnitz/gcode_import/gcode_import_gcode.inx create mode 100644 extensions/fablabchemnitz/gcode_import/gcode_import_nc.inx create mode 100644 extensions/fablabchemnitz/gcode_import/meta.json create mode 100644 extensions/fablabchemnitz/inventory_sticker/inventory_sticker.inx create mode 100644 extensions/fablabchemnitz/inventory_sticker/inventory_sticker.py create mode 100644 extensions/fablabchemnitz/inventory_sticker/meta.json create mode 100644 extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.inx create mode 100644 extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.py create mode 100644 extensions/fablabchemnitz/lasercut_jigsaw/meta.json create mode 100644 extensions/fablabchemnitz/paperfold/meta.json create mode 100644 extensions/fablabchemnitz/paperfold/paperfold.inx create mode 100644 extensions/fablabchemnitz/paperfold/paperfold.py create mode 100644 extensions/fablabchemnitz/styles_to_layers/meta.json create mode 100644 extensions/fablabchemnitz/styles_to_layers/styles_to_layers.inx create mode 100644 extensions/fablabchemnitz/styles_to_layers/styles_to_layers.py create mode 100644 extensions/fablabchemnitz/sudoku/meta.json create mode 100755 extensions/fablabchemnitz/sudoku/qqwing create mode 100644 extensions/fablabchemnitz/sudoku/qqwing.exe create mode 100644 extensions/fablabchemnitz/sudoku/sudoku.inx create mode 100644 extensions/fablabchemnitz/sudoku/sudoku.py create mode 100644 extensions/fablabchemnitz/vpypetools/meta.json create mode 100644 extensions/fablabchemnitz/vpypetools/vpype_logo.svg create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools.py create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_deduplicate.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_filter.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_freemode.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_linemerge.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_linesort.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_multipass.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_occult.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_relooping.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_splitall.inx create mode 100644 extensions/fablabchemnitz/vpypetools/vpypetools_trim.inx diff --git a/extensions/fablabchemnitz/animate_order/meta.json b/extensions/fablabchemnitz/animate_order/meta.json index 17a7ece..f32abb3 100644 --- a/extensions/fablabchemnitz/animate_order/meta.json +++ b/extensions/fablabchemnitz/animate_order/meta.json @@ -7,14 +7,14 @@ "original_name": "Animate Order", "original_id": "fablabchemnitz.de.animate_order", "license": "GNU GPL v3", - "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE", + "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE", "comment": "Written by Mario Voigt", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/animate_order", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/animate_order", "fork_url": null, "documentation_url": "https://stadtfabrikanten.org/display/IFM/Animate+Order", "inkscape_gallery_url": "https://inkscape.org/~MarioVoigt/%E2%98%85animate-order", "main_authors": [ - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py index 3bd5384..95641fa 100644 --- a/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py +++ b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py @@ -55,7 +55,6 @@ class CuttingOptimizer(inkex.EffectExtension): elements = self.svg.selected if len(elements) > 0: #if selection is existing, then we export only selected items to a new svg, which is then going to be processed. Otherwise we process the whole SVG document - extra_param = None template = self.svg.copy() for child in template.getchildren(): if child.tag == '{http://www.w3.org/2000/svg}defs': @@ -81,7 +80,7 @@ class CuttingOptimizer(inkex.EffectExtension): actions_list.append("export-filename:{}".format(svg_out)) actions_list.append("export-do") actions = ";".join(actions_list) - cli_output = inkscape(svg_out, extra_param, actions=actions) #process recent file + cli_output = inkscape(svg_out, actions=actions) #process recent file if len(cli_output) > 0: self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") self.msg(cli_output) diff --git a/extensions/fablabchemnitz/cutting_optimizer/meta.json b/extensions/fablabchemnitz/cutting_optimizer/meta.json index 1af2561..d7c5312 100644 --- a/extensions/fablabchemnitz/cutting_optimizer/meta.json +++ b/extensions/fablabchemnitz/cutting_optimizer/meta.json @@ -9,13 +9,13 @@ "license": "MIT License", "license_url": "https://github.com/thierry7100/CutOptim/blob/master/LICENSE", "comment": "", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/cutting_optimizer", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/cutting_optimizer", "fork_url": "https://github.com/thierry7100/CutOptim", "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018148", "inkscape_gallery_url": null, "main_authors": [ "github.com/thierry7100", - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/export_selection_as/export_selection_as.py b/extensions/fablabchemnitz/export_selection_as/export_selection_as.py index cd76043..e43aee8 100644 --- a/extensions/fablabchemnitz/export_selection_as/export_selection_as.py +++ b/extensions/fablabchemnitz/export_selection_as/export_selection_as.py @@ -66,8 +66,6 @@ class ExportObject(inkex.EffectExtension): scale_factor = self.svg.unittouu("1px") svg_export = self.options.export_svg - #extra_param = "--batch-process" - extra_param = None if self.options.export_svg is False and \ self.options.export_dxf is False and \ @@ -179,7 +177,7 @@ class ExportObject(inkex.EffectExtension): actions_list.append("export-filename:{}".format(svg_out)) actions_list.append("export-do") actions = ";".join(actions_list) - cli_output = inkscape(svg_out, extra_param, actions=actions) #process recent file + cli_output = inkscape(svg_out, actions=actions) #process recent file if len(cli_output) > 0: self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") self.msg(cli_output) @@ -217,7 +215,7 @@ class ExportObject(inkex.EffectExtension): inkex.utils.debug("%d %s %s" % (proc.returncode, stdout, stderr)) if self.options.export_pdf is True: - cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions='export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do'.format(file_name=os.path.join(export_dir, filename_base + '.pdf'))) + cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), actions='export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do'.format(file_name=os.path.join(export_dir, filename_base + '.pdf'))) if len(cli_output) > 0: self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") self.msg(cli_output) @@ -236,7 +234,7 @@ class ExportObject(inkex.EffectExtension): actions_list.append("export-filename:{}".format(png_export)) actions_list.append("export-do") actions = ";".join(actions_list) - cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions) + cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), actions=actions) if len(cli_output) > 0: self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") self.msg(cli_output) @@ -256,7 +254,7 @@ class ExportObject(inkex.EffectExtension): actions_list.append("export-filename:{}".format(png_export)) actions_list.append("export-do") actions = ";".join(actions_list) - cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions) + cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), actions=actions) if len(cli_output) > 0: self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") self.msg(cli_output) diff --git a/extensions/fablabchemnitz/export_selection_as/meta.json b/extensions/fablabchemnitz/export_selection_as/meta.json index 0ca17f8..2fe4afe 100644 --- a/extensions/fablabchemnitz/export_selection_as/meta.json +++ b/extensions/fablabchemnitz/export_selection_as/meta.json @@ -9,13 +9,13 @@ "license": "MIT License", "license_url": "https://github.com/mireq/inkscape-export-selection-as-svg/blob/master/LICENSE", "comment": "", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/export_selection_as", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/export_selection_as", "fork_url": "https://github.com/mireq/inkscape-export-selection-as-svg", "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=104923223", "inkscape_gallery_url": null, "main_authors": [ "github.com/mireq", - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/gcode_import/gcode_import.py b/extensions/fablabchemnitz/gcode_import/gcode_import.py new file mode 100644 index 0000000..8774c97 --- /dev/null +++ b/extensions/fablabchemnitz/gcode_import/gcode_import.py @@ -0,0 +1,673 @@ +#!/usr/bin/env python3 +""" +ImportGCode, and Inkscape extension by Nathaniel Klumb + +This extension adds support for some GCode files to the File/Import... +dialog in Inkscape. It loads the GCode file passed to it by Inkscape as a +command-line parameter and writes the resulting SVG to stdout (which is how +Inkscape input plugins work). + +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. +""" +import re +import inkex +import sys +import argparse +from io import StringIO +from math import sqrt ,pi, sin, cos, tan, acos, atan2, fabs +from lxml import etree + +class ImportGCode: + """ Import a GCode file and process it into an SVG. """ + current_id = 0 + geometry_error = False + + def __init__(self,gcode_filename,v_carve=False,laser_mode=False, + ignore_z=True,label_z=True, + tool_diameter=1.0,v_angle=90.0,v_top=0.0,v_step=1.0): + """ Load a GCode file and process it into an SVG. """ + self.unit = 1.0 + self.ignore_z = ignore_z or v_carve + self.label_z = label_z and not v_carve + self.tool_diameter = tool_diameter + self.v_carve = v_carve + self.v_angle = v_angle * pi / 180.0 + self.v_top = v_top + self.v_step = v_step + self.laser_mode = laser_mode + self.spindle = False + self.speed = 0 + with open(gcode_filename) as file: + self.loadGCode(file) + self.createSVG() + + def getIJ(self,x1,y1,x2,y2,r): + """ Calculate I and J from two arc endpoints and a radius. """ + theta = atan2(y2 - y1, x2 - x1) + alpha = acos(sqrt((x2 - x1)**2 + (y2 - y1)**2)/(2 * abs(r))) + return (r * cos(theta + alpha), r * sin(theta + alpha)) + + def getTangentPoints(self,x1,y1,r1,x2,y2,r2): + """ Compute the four outer tangent endpoints of two circles. """ + theta = atan2(y2 - y1, x2 - x1) + try: + alpha = acos((r1 - r2)/sqrt((x2 - x1)**2 + (y2 - y1)**2)) + except ValueError: + # It's broken, but we'll just cap it off. + # The SVG will be messed up, but that's better feedback + # than just blankly saying, "Sorry, please try again." + if not self.geometry_error: #Only show the error once. + inkex.errormsg('Math error importing V-carve: ' + 'V-bit angle too large?') + inkex.errormsg(' Check your included angle ' + 'setting and try again.') + self.geometry_error = True + if ((r1 - r2)/sqrt((x2 - x1)**2 + (y2 - y1)**2) < -1): + alpha = pi + else: + alpha = 0 + return ((x1 + r1 * cos(theta - alpha), y1 + r1 * sin(theta - alpha)), + (x1 + r1 * cos(theta + alpha), y1 + r1 * sin(theta + alpha)), + (x2 + r2 * cos(theta + alpha), y2 + r2 * sin(theta + alpha)), + (x2 + r2 * cos(theta - alpha), y2 + r2 * sin(theta - alpha))) + + def intersectLines(self, p1, p2, p3, p4): + """ Calculate the intersection of Line(p1,p2) and Line(p3,p4) + + returns a tuple: (x, y, valid, included) + (x, y): the intersection + valid: a unique solution exists + included: the solution is within both the line segments + Segment(p1,p2) and Segment(p3,p4) + """ + + DET_TOLERANCE = 0.00000001 + T = 0.00000001 + + # the first line is pt1 + r*(p2-p1) + x1,y1 = p1 + x2,y2 = p2 + dx1 = x2 - x1 + dy1 = y2 - y1 + + # the second line is p4 + s*(p4-p3) + x3,y3 = p3 + x4,y4 = p4; + dx2 = x4 - x3 + dy2 = y4 - y3 + + # In matrix form: + # [ dx1 -dx2 ][ r ] = [ x3-x1 ] + # [ dy1 -dy2 ][ s ] = [ y3-y1 ] + # + # Which can be solved: + # [ r ] = _1_ [ -dy2 dx2 ] [ x3-x1 ] + # [ s ] = DET [ -dy1 dx1 ] [ y3-y1 ] + # + # With the deteminant: DET = (-dx1 * dy2 + dy1 * dx2) + DET = (-dx1 * dy2 + dy1 * dx2) + + # If DET is zero, they're parallel + if fabs(DET) < DET_TOLERANCE: + # If they overlap, either p3 or p4 must be + # an included point, so check one, then check the + # other. If either falls inside the segment from + # p1 to p2, return it as *a* valid intersection. + # Otherwise, return the bad news -- no intersection. + # + # Also, when checking the limits, allow a tolerance, T, + # since we're working in floating point. + if ((((x3 >= x1 - T) and (x3 <= x2 + T)) or + ((x3 <= x1 + T) and (x3 >= x2 - T))) and + (((y3 >= y1 - T) and (y3 <= y2 + T)) or + ((y3 <= y1 + T) and (y3 >= y2 - T)))): + return (x3,y3,False,True) + elif ((((x4 >= x1 - T) and (x4 <= x2 + T)) or + ((x4 <= x1 + T) and (x4 >= x2 - T))) and + (((y4 >= y1 - T) and (y4 <= y2 + T)) or + ((y4 <= y1 + T) and (y4 >= y2 - T)))): + return (x4,y4,False,True) + # NO CONNECTION... *dialtone* + else: + return (None,None,False,False) + + # Since the determinant is non-zero, now take the reciprocal. + invDET = 1.0/DET + + # We want to calculate the intersection for each line so we can + # average the results together. They should be identical, but + # floating-point and rounding error, etc... + # Calculate the scalar distances along Line(p1,p2) and Line(p3,p4) + r = invDET * (-dy2 * (x3-x1) + dx2 * (y3-y1)) + s = invDET * (-dy1 * (x3-x1) + dx1 * (y3-y1)) + + # Average the intersection's coordinates from the two lines. + x = (x1 + r*dx1 + x3 + s*dx2)/2.0 + y = (y1 + r*dy1 + y3 + s*dy2)/2.0 + + # Now one last check to see if the intersection's coordinates are + # included within both line segments. + included = ((((x >= x1 - T) and (x <= x2 + T)) or + ((x <= x1 + T) and (x >= x2 - T))) and + (((y >= y1 - T) and (y <= y2 + T)) or + ((y <= y1 + T) and (y >= y2 - T))) and + (((x >= x3 - T) and (x <= x4 + T)) or + ((x <= x3 + T) and (x >= x4 - T))) and + (((y >= y3 - T) and (y <= y4 + T)) or + ((y <= y3 + T) and (y >= y4 - T)))) + return (x,y,True,included) + + def getRadius(self,Z): + """ Compute the radius of a V-bit given a Z coordinate. + + If the V-bit is above stock top, we just mirror it. + Technically, the file's broken, but hey, may as well do something. + """ + if (self.v_top <= Z): + return (Z - self.v_top) * tan(self.v_angle / 2) + else: + return (self.v_top - Z) * tan(self.v_angle / 2) + + def getAngle(self,center,point): + """ Calculate the angle from a center to a point. """ + a = atan2(point[1] - center[1], point[0] - center[0]) + return a + ((2*pi) if (a<0.0) else 0) + + def isLargeAngle(self,center,p1,p2): + """ Determine whether the SVG large angle flag should be set. """ + a1 = self.getAngle(center,p1) + a2 = self.getAngle(center,p2) + angle = a1 - a2 + if angle < 0: + angle += 2 * pi + return 1 if (abs(angle) > pi) else 0 + + def interpolatePoints(self,center,p1,p2): + """ Interpolate a set of points along an arc. """ + a1 = self.getAngle(center,p1) + a2 = self.getAngle(center,p2) + angle = a2 - a1 + dz = 1.0 * (p2[2]-p1[2]) + r = sqrt((center[0] - p1[0])**2 + (center[1] - p1[1])**2) + length = r * abs(angle) / pi + steps = int(round(length/self.v_step)) + points = [] + for i in range(1,steps): + point = (center[0] + r * cos(a1 + angle*i/steps), + center[1] + r * sin(a1 + angle*i/steps), + p1[2] + dz*i/steps) + points += [point] + points += [p2] + return points + + def makeVcarve(self,v_segments): + """ Connect multiple V-carve segments into one path. + + Start on one V-carve segment and chain all the way to the + opposite end, then add a switchback and chain all the way + back to the beginning. + """ + vs = v_segments + # Move to the starting point. + path = 'M {} {} '.format(vs[0][1][0][0],vs[0][1][0][1]) + # Initial arc, if it's not a point. + if vs[0][0][0][2] > 0: + path += ('A {} {} 0 {} {} {} {} ' + ).format(vs[0][0][0][2],vs[0][0][0][2], + 1 if (vs[0][0][0][2] > vs[0][0][1][2]) else 0, + 0,vs[0][1][1][0],vs[0][1][1][1]) + # Step through all the segments on the way to the other end. + for v in range(len(vs)-1): + # Check whether an intersection exists between the two + # line segments. If so, use it, otherwise, connect with an arc. + x,y,valid,included = self.intersectLines(vs[v][1][1], + vs[v][1][2], + vs[v+1][1][1], + vs[v+1][1][2]) + if included: #line segments + path += 'L {} {} '.format(x,y) + else: + path += ('L {} {} A {} {} 0 {} {} {} {} ' + ).format(vs[v][1][2][0],vs[v][1][2][1], + vs[v][0][1][2],vs[v][0][1][2], + self.isLargeAngle(vs[v][0][1], + vs[v][1][2], + vs[v+1][1][1]), + 0,vs[v+1][1][1][0],vs[v+1][1][1][1]) + # Connecting line. + path += 'L {} {} '.format(vs[len(vs)-1][1][2][0], + vs[len(vs)-1][1][2][1]) + # Switchback arc, if it's not a point. + if vs[len(vs)-1][0][1][2] > 0: + path += ('A {} {} 0 {} {} {} {} ' + ).format(vs[len(vs)-1][0][1][2],vs[len(vs)-1][0][1][2], + 1 if (vs[len(vs)-1][0][1][2] > + vs[len(vs)-2][0][0][2]) else 0, + 0,vs[len(vs)-1][1][3][0],vs[len(vs)-1][1][3][1]) + # Step through all the segments on the way back home. + for v in range(len(vs)-1,0,-1): + # Check whether an intersection exists between the two + # line segments. If so, use it, otherwise, connect with an arc. + x,y,valid,included = self.intersectLines(vs[v][1][3], + vs[v][1][0], + vs[v-1][1][3], + vs[v-1][1][0]) + if included: #line segments + path += 'L {} {} '.format(x,y) + else: + path += ('L {} {} A {} {} 0 {} {} {} {} ' + ).format(vs[v][1][0][0],vs[v][1][0][1], + vs[v-1][0][1][2],vs[v-1][0][1][2], + self.isLargeAngle(vs[v-1][0][1], + vs[v][1][0], + vs[v-1][1][3]), + 0,vs[v-1][1][3][0],vs[v-1][1][3][1]) + # And finally, close the curve. + path += 'Z' + return path + + def getVsegment(self,x1,y1,z1,x2,y2,z2): + """ Compute the required data to define a V-carve segment. """ + r1 = self.getRadius(z1) + r2 = self.getRadius(z2) + p = self.getTangentPoints(x1,y1,r1,x2,y2,r2) + return (((x1,y1,r1),(x2,y2,r2)),p) + + def parseLine(self,command,X,Y,Z,line,no_path=False): + """ Parse a line of G-code. + + This takes the current coordinates and modal command, then processes + the new line of G-code to yield a new ending set of coordinates + plus values necessary for curve computations. It also returns the + resulting path data, unless otherwise indicated, e.g. for V-carves. + """ + comments = re.compile('\([^\)]*\)') + commands = re.compile('([MSGXYZIJKR])([-.0-9]+)') + + lastX = X + lastY = Y + lastZ = Z + I = 0.0 + J = 0.0 + K = 0.0 + R = None + results = commands.findall(comments.sub('',line)) + if not line.startswith(";"): + inkex.utils.debug(results) + + for (code,val) in results: + v = float(val) + i = int(v) + + if code == 'M': + if i == 3: + self.spindle = True + elif i == 5: + self.spindle = False + elif code == 'S': + self.speed = v + elif code == 'G': + if i == 0: + command = 'G0' + elif i == 1: + command = 'G1' + elif i == 2: + command = 'G2' + elif i == 3: + command = 'G3' + elif i == 20: + self.unit = 25.4 + elif i == 21: + self.unit = 1.0 + elif val == "90": + self.absolute = True + elif val == "91": + self.absolute = False + elif val == "90.1": + self.absoluteIJK = True + elif val == "91.1": + self.absoluteIJK = False + elif code == 'X': + if self.absolute: + X = v * self.unit + else: + X += v * self.unit + elif code == 'Y': + if self.absolute: + Y = v * self.unit + else: + Y += v * self.unit + elif code == 'Z': + if self.absolute: + Z = v * self.unit + else: + Z += v * self.unit + elif code == 'I': + I = v * self.unit + if self.absoluteIJK: + I -= X + elif code == 'J': + J = v * self.unit + if self.absoluteIJK: + J -= Y + elif code == 'K': + # Sure, process it, but we don't *do* anything with K. + K = v * self.unit + if self.absoluteIJK: + K -= Z + elif code == 'R': + R = v * self.unit + + if no_path: # V-carving doesn't need any path data. + return ((command, X, Y, Z, I, J, K, R, '')) + + # The line's been parsed. Now let's generate path data from it. + path = '' + if command == 'G1': + # If there's any XY motion, make a line segment. + if ((X != lastX) or (Y != lastY)): + path = 'L {} {} '.format(round(X,5),round(Y,5)) + elif (command == 'G2') or (command == 'G3'): + # Arcs! Oh, what glorious fun we'll have! First, sweep direction. + sweep = 0 if (command == 'G2') else 1 + # R overrules I and J if both are present, so we compute + # new I and J values based on R. We need those to determine + # whether the Large Angle Flag needs to be set. + if R is not None: + I,J = self.getIJ(lastX,lastY,X,Y,R) + if (I != 0.0) or (J != 0.0): + if sweep == 0: + large_arc = self.isLargeAngle((lastX+I,lastY+J), + (lastX,lastY),(X,Y)) + else: + large_arc = self.isLargeAngle((lastX+I,lastY+J), + (X,Y),(lastX,lastY)) + radius = sqrt(I**2 + J**2) + path = 'A {} {} 0 {} {} {} {} '.format(round(radius,5), + round(radius,5), + large_arc,sweep, + round(X,5),round(Y,5)) + # No R, and no I or J either? Let's just call it a line segment. + # (It may have had a K, but we don't believe in K for SVG imports.) + else: + path = 'L {} {} '.format(round(X,5),round(Y,5)) + + # In laser mode, if the spindle isn't active or the speed is zero, + # there's no lasing to be had. Drop the path data. (The Inkscape + # extension from J Tech Photonics uses G1/G2/G3 moves throughout, + # with nary a G0, so if we don't do this, we'll show unlasered paths.) + if (self.laser_mode and ((not self.spindle) or (self.speed == 0))): + path = '' + return ((command, X, Y, Z, I, J, K, R, path)) + + def savePath(self,path,Z): + """ Save a set of path data, filing it by Z if appropriate. """ + if (path.find('A') == -1) and (path.find('L') == -1): + return #empty path + if self.ignore_z: + if path not in self.paths: + self.paths.add(path) + else: + try: + if path not in self.paths_by_z[Z]: + self.paths_by_z[Z].add(path) + except KeyError: + self.paths_by_z[Z] = set([path]) + + def loadGCode(self,gcode_file): + """ Load a G-code file, handling the contents. """ + if self.ignore_z: + self.paths = set([]) + else: + self.paths_by_z = {} + self.absolute = True + self.absoluteIJK = False + self.unit=1.0 + command = '' + X = 0.0 + Y = 0.0 + Z = 0.0 + lastX = X + lastY = Y + lastZ = Z + self.minX = 0.0 + self.minY = 0.0 + self.minZ = 0.0 + self.maxX = 0.0 + self.maxY = 0.0 + self.maxZ = 0.0 + + path = '' + line = gcode_file.readline() + v_segments = [] + while line: + command,X,Y,Z,I,J,K,R,path_data = self.parseLine(command, + X, Y, Z, line, + self.v_carve) + self.minX = X if X < self.minX else self.minX + self.maxX = X if X > self.maxX else self.maxX + self.minY = Y if Y < self.minY else self.minY + self.maxY = Y if Y > self.maxY else self.maxY + self.minZ = Z if Z < self.minZ else self.minZ + self.maxZ = Z if Z > self.maxZ else self.maxZ + + # V-carve mode. + if self.v_carve: + if (lastX != X) or (lastY != Y): + if command == 'G1': + v_segments += [self.getVsegment(lastX, lastY, lastZ, + X, Y, Z)] + elif (command == 'G2') or (command == 'G3'): + # We don't attempt to handle the plethora of curves + # that can result from V-carving arcs. Instead, we + # just interpolate them and process the subparts. + points = self.interpolatePoints((lastX+I,lastY+J), + (lastX,lastY,lastZ), + (X,Y,Z)) + iX = lastX + iY = lastY + iZ = lastZ + for p in points: + v_segments += [self.getVsegment(iX, iY, iZ, + p[0], p[1], p[2])] + iX = p[0] + iY = p[1] + iZ = p[2] + else: + if len(v_segments): + self.savePath(self.makeVcarve(v_segments),'VCarve') + v_segments = [] + # Standard mode (non-V-carve). + else: + if ((command == 'G0') or + (not self.ignore_z and (Z != lastZ)) or + (self.laser_mode and ((not self.spindle) or + (self.speed == 0)))): + if (path != ''): + self.savePath(path,lastZ) + path = '' + if (((command == 'G1') or + (command == 'G2') or + (command == 'G3')) and (path == '')): + path = 'M {} {} {}'.format(lastX,lastY,path_data) + else: + path += path_data + lastX = X + lastY = Y + lastZ = Z + line = gcode_file.readline() + # Always remember to save the tail end of your work. + if self.v_carve: + if len(v_segments): + self.savePath(self.makeVcarve(v_segments),'VCarve') + else: + if (path != ''): + self.savePath(path,lastZ) + + def filterPaths(self): + """ Filter out duplicate paths, leaving only the deepest instance. """ + if self.ignore_z: + return + z_depths = sorted(self.paths_by_z,None,None,True) + for i in range(0,len(z_depths)): + for j in range(i+1, len(z_depths)): + self.paths_by_z[z_depths[i]] -= self.paths_by_z[z_depths[j]] + + def next_id(self): + """ Return an incrementing value. """ + self.current_id += 1 + return self.current_id + + def getStyle(self,color='#000000',width=None): + """ Create a CSS-type style string. """ + if width is None: + width = self.tool_diameter + return ('opacity:1;vector-effect:none;fill:none;fill-opacity:1;' + 'stroke:{};stroke-width:{};stroke-opacity:1;' + 'stroke-linecap:round;stroke-linejoin:round;' + 'stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0' + ).format(color,width) + + def createSVG(self): + """ Create the output SVG. """ + base = ('' + ).format(self.maxX-self.minX, self.maxY-self.minY, + self.minX, self.minY, self.maxX-self.minX, self.maxY-self.minY) + self.doc = etree.parse(StringIO((base))) + svg = self.doc.getroot() + # Since G-code and SVG interpret Y in opposite directions, + # we just group everything under a transform that mirrors Y. + svg = etree.SubElement(svg,'g',{'id':'gcode', + 'transform':'scale(1,-1)'}) + # Add illustrative axes to the SVG to facilitate positioning. + etree.SubElement(svg,'path', + {'d':'M 0 {} V {}'.format(self.minY, self.maxY), + 'style':self.getStyle('#00ff00',0.5), + 'id':'vertical'}) + etree.SubElement(svg,'path', + {'d':'M {} 0 H {}'.format(self.minX, self.maxX), + 'style':self.getStyle('#ff0000',0.5), + 'id':'horizontal'}) + # For V-carves, include the paths and use a narrow stroke width. + if self.v_carve: + for path in self.paths: + etree.SubElement(svg,'path', + {'d':path, + 'style':self.getStyle(width=0.1), + 'id':'path{}'.format(self.next_id())}) + # For standard mode with Z ignored, include the paths. + elif self.ignore_z: + for path in self.paths: + etree.SubElement(svg,'path', + {'d':path, + 'style':self.getStyle(), + 'id':'path{}'.format(self.next_id())}) + # For standard mode with Z grouping, filter the paths, + # then add each group of paths (and optionally, labels). + else: + self.filterPaths() + z_depths = sorted(self.paths_by_z) + depth_num = 0 + for i in range(0,len(z_depths)): + if len(self.paths_by_z[z_depths[i]]): + params = {'id':('group{}-{}' + ).format(self.next_id(),z_depths[i]), + 'style':self.getStyle()} + group = etree.SubElement(svg,'g',params) + + # If labels are enabled, add the label to the group. + if self.label_z: + params = {'x':'{}'.format(self.maxX), + 'y':'{}'.format(depth_num*-5), + 'transform':'scale(1,-1)', + 'id':'text{}'.format(i), + 'style':('opacity:1;fill:#0000ff;' + 'fill-opacity:1;stroke:none;' + 'font-size:4.5')} + if self.unit == 1.0: + label = '{} mm'.format(z_depths[i]) + else: + label = '{} in'.format(z_depths[i]/self.unit) + etree.SubElement(group,'text',params + ).text = label + + depth_num += 1 + + # Now add the paths to the group. + for path in self.paths_by_z[z_depths[i]]: + id = 'path{}'.format(self.next_id()) + etree.SubElement(group,'path', + {'d':path, + 'style':self.getStyle(), + 'id':id}) + # If labels are enabled, label the labels. + if self.label_z: + etree.SubElement(svg,'text', + {'x':'{}'.format(self.maxX), + 'y':'{}'.format(depth_num*-5), + 'transform':'scale(1,-1)', + 'id':'text{}'.format(i), + 'style':('opacity:1;fill:#0000ff;' + 'fill-opacity:1;stroke:none;' + 'font-size:4.5')} + ).text = 'Z Groups:' + +# And now for the code to allow Inkscape to run our lovely extension. +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=('usage: %prog [options] GCodeFile')) + parser.add_argument('-m', '--mode', help='Mode: vcarve, standard, laser', default='standard') + parser.add_argument('-a', '--v_angle', help='Included (full) angle for V-bit, in degrees.', default=None) + parser.add_argument('-t', '--v_top', help='Stock top (usually zero)', default=None) + parser.add_argument('-s', '--v_step', help='Step size for curve interpolation.', default=None) + parser.add_argument('-d', '--tool_diameter', help='Tool diameter / path width.', default=None) + parser.add_argument('-u', '--units', help='Dialog units.', default='mm') + parser.add_argument('-z', '--z_axis', help='Z-axis: ignore,group,label', default=False) + parser.add_argument('--tab') + parser.add_argument('--inputhelp') + parser.add_argument('inputfile') + + # Now, process, my lovelies! + args = parser.parse_args() + + # First steps first, what mode? + v_carve = False + ignore_z = False + laser_mode = False + if (args.mode == 'vcarve'): + v_carve = True + elif (args.mode == 'laser'): + laser_mode = True + + # V-carve parameters. + try: + v_angle = round(float(args.v_angle),3) + except ValueError: + v_angle = 1.0 + try: + v_top = round(float(args.v_top) * + (25.4 if (args.units == 'in') else 1.0),5) + except ValueError: + v_top = 0.0 + try: + v_step = round(float(args.v_step) * + (25.4 if (args.units == 'in') else 1.0),5) + except ValueError: + v_step = 1.0 + + # Standard parameters. + try: + diameter = round(float(args.tool_diameter) * + (25.4 if (args.units == 'in') else 1.0),3) + except ValueError: + diameter = 1.0 + + # General args. + ignore_z = (args.z_axis == 'ignore') + label_z = (args.z_axis == 'label') + + gc = ImportGCode(args.inputfile, v_carve, laser_mode, ignore_z, label_z, diameter, v_angle, v_top, v_step) + gc.doc.write(sys.stdout.buffer) \ No newline at end of file diff --git a/extensions/fablabchemnitz/gcode_import/gcode_import_gcode.inx b/extensions/fablabchemnitz/gcode_import/gcode_import_gcode.inx new file mode 100644 index 0000000..be0c389 --- /dev/null +++ b/extensions/fablabchemnitz/gcode_import/gcode_import_gcode.inx @@ -0,0 +1,54 @@ + + + GCode Import (*.gcode) + fablabchemnitz.de.gcode_import.gcode + + + + + + + + + 90 + 0 + 0 + + 6.35 + + + + + + + + + + + + + + + + + .gcode + application/x-gcode + GCode File (*.gcode) + Import GCode File + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/gcode_import/gcode_import_nc.inx b/extensions/fablabchemnitz/gcode_import/gcode_import_nc.inx new file mode 100644 index 0000000..1fb6bb8 --- /dev/null +++ b/extensions/fablabchemnitz/gcode_import/gcode_import_nc.inx @@ -0,0 +1,54 @@ + + + GCode Import (*.nc) + fablabchemnitz.de.gcode_import.nc + + + + + + + + + 90 + 0 + 0 + + 6.35 + + + + + + + + + + + + + + + + + .nc + application/x-gcode + GCode File (*.nc) + Import GCode File + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/gcode_import/meta.json b/extensions/fablabchemnitz/gcode_import/meta.json new file mode 100644 index 0000000..42071dd --- /dev/null +++ b/extensions/fablabchemnitz/gcode_import/meta.json @@ -0,0 +1,21 @@ +[ + { + "name": "GCode Import ", + "id": "fablabchemnitz.de.gcode_import", + "path": "gcode_import.", + "dependent_extensions": null, + "original_name": "", + "original_id": "com.ClayJar.GCodeImport.", + "license": "GNU GPL v2", + "license_url": "https://github.com/ClayJarCom/ImportGCode/blob/master/LICENSE", + "comment": "", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/gcode_import", + "fork_url": "https://github.com/ClayJarCom/ImportGCode", + "documentation_url": "https://stadtfabrikanten.org/display/IFM/GCode+Import", + "inkscape_gallery_url": null, + "main_authors": [ + "github.com/ClayJarCom", + "github.com/vmario89" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/guilloche_creations/meta.json b/extensions/fablabchemnitz/guilloche_creations/meta.json index 862ca78..190b156 100644 --- a/extensions/fablabchemnitz/guilloche_creations/meta.json +++ b/extensions/fablabchemnitz/guilloche_creations/meta.json @@ -9,14 +9,14 @@ "license": "GNU GPL v3", "license_url": "https://inkscape.org/de/~DrWiggly/%E2%98%85guillocheextensions-for-v1x", "comment": "fork of https://inkscape.org/de/~fluent_user/%E2%98%85guilloche-pattern-extension", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/guilloche_creations", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/guilloche_creations", "fork_url": "https://inkscape.org/de/~DrWiggly/%E2%98%85guillocheextensions-for-v1x", "documentation_url": "https://stadtfabrikanten.org/display/IFM/Guilloche+Pattern", "inkscape_gallery_url": null, "main_authors": [ "inkscape.org/fluent_user", "inkscape.org/DrWiggly", - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.inx b/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.inx new file mode 100644 index 0000000..2f12498 --- /dev/null +++ b/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.inx @@ -0,0 +1,62 @@ + + + Inventory Sticker + fablabchemnitz.de.inventory_sticker + + + + https://the.domain.de/items.csv + User + Password + + qwa.es + Stadtfabrikanten e.V. + + 1 + /home/ + false + true + false + 0 + 04f9:2044 + false + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.py b/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.py new file mode 100644 index 0000000..ae307b0 --- /dev/null +++ b/extensions/fablabchemnitz/inventory_sticker/inventory_sticker.py @@ -0,0 +1,652 @@ +#!/usr/bin/env python3 +# +# An extension to generate SVG/PNG labels (stickers) for use with our item inventory system. +# It pulls a .csv file from a server URL (protected by basic auth) and exports and prints the labels to a Brother QL-720NW label printer +# Documentation: https://wiki.fablabchemnitz.de/display/TEED/Werkstattorientierung+im+FabLab+-+Digtales+Inventar +# +# Made by FabLab Chemnitz / Stadtfabrikanten e.V. - Developer: Mario Voigt (year 2021) +# +# This extension is based on the "original" barcode extension included in default InkScape Extension Set, which is licensed by the following: +# +# Copyright (C) 2009 John Beard john.j.beard@gmail.com +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import csv +import os +import shutil +import urllib.request +from lxml import etree +import inkex +from inkex import Rectangle +from inkex.command import inkscape +import re +import subprocess +from subprocess import Popen, PIPE + +INVALID_BIT = 2 + +# CODEWORD STREAM GENERATION ========================================= +# take the text input and return the codewords, +# including the Reed-Solomon error-correcting codes. +# ===================================================================== + +def get_codewords(text, nd, nc, inter, size144): + # convert the data to the codewords + data = list(encode_to_ascii(text)) + + if not size144: # render a "normal" datamatrix + data_blocks = partition_data(data, nd * inter) # partition into data blocks of length nd*inter -> inter Reed-Solomon block + data_blocks = interleave(data_blocks, inter) # interleave consecutive inter blocks if required + data_blocks = reed_solomon(data_blocks, nd, nc) # generate and append the Reed-Solomon codewords + data_blocks = combine_interleaved(data_blocks, inter, nd, nc, False) # concatenate Reed-Solomon blocks bound for the same datamatrix + return data_blocks + + +# Takes a codeword stream and splits up into "inter" blocks. +# eg interleave( [1,2,3,4,5,6], 2 ) -> [1,3,5], [2,4,6] +def interleave(blocks, inter): + if inter == 1: # if we don"t have to interleave, just return the blocks + return blocks + else: + result = [] + for block in blocks: # for each codeword block in the stream + block_length = int(len(block) / inter) # length of each interleaved block + inter_blocks = [[0] * block_length for i in range(inter)] # the interleaved blocks + + for i in range(block_length): # for each element in the interleaved blocks + for j in range(inter): # for each interleaved block + inter_blocks[j][i] = block[i * inter + j] + + result.extend(inter_blocks) # add the interleaved blocks to the output + + return result + +# Combine interleaved blocks into the groups for the same datamatrix +# +# e.g combine_interleaved( [[d1, d3, d5, e1, e3, e5], [d2, d4, d6, e2, e4, e6]], 2, 3, 3 ) +# --> [[d1, d2, d3, d4, d5, d6, e1, e2, e3, e4, e5, e6]] +def combine_interleaved(blocks, inter, nd, nc, size144): + if inter == 1: # the blocks aren"t interleaved + return blocks + else: + result = [] + for i in range(len(blocks) // inter): # for each group of "inter" blocks -> one full datamatrix + data_codewords = [] # interleaved data blocks + + if size144: + nd_range = 1558 # 1558 = 156*8 + 155*2 + nc_range = 620 # 620 = 62*8 + 62*2 + else: + nd_range = nd * inter + nc_range = nc * inter + + for j in range(nd_range): # for each codeword in the final list + data_codewords.append(blocks[i * inter + j % inter][j // inter]) + + for j in range(nc_range): # for each block, add the ecc codewords + data_codewords.append(blocks[i * inter + j % inter][nd + j // inter]) + + result.append(data_codewords) + return result + +def encode_to_ascii(text): + """Encode this text into chunks, ascii or digits""" + i = 0 + while i < len(text): + # check for double digits, if the next char is also a digit + if text[i].isdigit() and (i < len(text) - 1) and text[i + 1].isdigit(): + yield int(text[i] + text[i + 1]) + 130 + i += 2 # move on 2 characters + else: # encode as a normal ascii, + yield ord(text[i]) + 1 # codeword is ASCII value + 1 (ISO 16022:2006 5.2.3) + i += 1 # next character + +# partition data into blocks of the appropriate size to suit the +# Reed-Solomon block being used. +# e.g. partition_data([1,2,3,4,5], 3) -> [[1,2,3],[4,5,PAD]] +def partition_data(data, rs_data): + PAD_VAL = 129 # PAD codeword (ISO 16022:2006 5.2.3) + data_blocks = [] + i = 0 + while i < len(data): + if len(data) >= i + rs_data: # we have a whole block in our data + data_blocks.append(data[i:i + rs_data]) + i = i + rs_data + else: # pad out with the pad codeword + data_block = data[i:len(data)] # add any remaining data + pad_pos = len(data) + padded = False + while len(data_block) < rs_data: # and then pad with randomised pad codewords + if not padded: + data_block.append(PAD_VAL) # add a normal pad codeword + padded = True + else: + data_block.append(randomise_pad_253(PAD_VAL, pad_pos)) + pad_pos += 1 + data_blocks.append(data_block) + break + + return data_blocks + + +# Pad character randomisation, to prevent regular patterns appearing +# in the data matrix +def randomise_pad_253(pad_value, pad_position): + pseudo_random_number = ((149 * pad_position) % 253) + 1 + randomised = pad_value + pseudo_random_number + if randomised <= 254: + return randomised + else: + return randomised - 254 + +# REED-SOLOMON ENCODING ROUTINES ===================================== + +# "prod(x,y,log,alog,gf)" returns the product "x" times "y" +def prod(x, y, log, alog, gf): + if x == 0 or y == 0: + return 0 + else: + result = alog[(log[x] + log[y]) % (gf - 1)] + return result + +# generate the log & antilog lists: +def gen_log_alog(gf, pp): + log = [0] * gf + alog = [0] * gf + + log[0] = 1 - gf + alog[0] = 1 + + for i in range(1, gf): + alog[i] = alog[i - 1] * 2 + + if alog[i] >= gf: + alog[i] = alog[i] ^ pp + + log[alog[i]] = i + + return log, alog + + +# generate the generator polynomial coefficients: +def gen_poly_coeffs(nc, log, alog, gf): + c = [0] * (nc + 1) + c[0] = 1 + + for i in range(1, nc + 1): + c[i] = c[i - 1] + + j = i - 1 + while j >= 1: + c[j] = c[j - 1] ^ prod(c[j], alog[i], log, alog, gf) + j -= 1 + + c[0] = prod(c[0], alog[i], log, alog, gf) + + return c + + +# "ReedSolomon(wd,nd,nc)" takes "nd" data codeword values in wd[] +# and adds on "nc" check codewords, all within GF(gf) where "gf" is a +# power of 2 and "pp" is the value of its prime modulus polynomial */ +def reed_solomon(data, nd, nc): + # parameters of the polynomial arithmetic + gf = 256 # operating on 8-bit codewords -> Galois field = 2^8 = 256 + pp = 301 # prime modulus polynomial for ECC-200 is 0b100101101 = 301 (ISO 16022:2006 5.7.1) + + log, alog = gen_log_alog(gf, pp) + c = gen_poly_coeffs(nc, log, alog, gf) + + for block in data: # for each block of data codewords + + block.extend([0] * (nc + 1)) # extend to make space for the error codewords + + # generate "nc" checkwords in the list block + for i in range(0, nd): + k = block[nd] ^ block[i] + + for j in range(0, nc): + block[nd + j] = block[nd + j + 1] ^ prod(k, c[nc - j - 1], log, alog, gf) + + block.pop() + + return data + + +# MODULE PLACEMENT ROUTINES=========================================== +# These routines take a steam of codewords, and place them into the +# DataMatrix in accordance with Annex F of BS ISO/IEC 16022:2006 + +def bit(byte, bit_ch): + """bit() returns the bit"th bit of the byte""" + # the MSB is bit 1, LSB is bit 8 + return (byte >> (8 - bit_ch)) % 2 + +def module(array, nrow, ncol, row, col, bit_ch): + """place a given bit with appropriate wrapping within array""" + if row < 0: + row = row + nrow + col = col + 4 - ((nrow + 4) % 8) + + if col < 0: + col = col + ncol + row = row + 4 - ((ncol + 4) % 8) + + array[row][col] = bit_ch + +def place_square(case, array, nrow, ncol, row, col, char): + """Populate corner cases (0-3) and utah case (-1)""" + for i in range(8): + x, y = [ + [(row - 1, 0), (row - 1, 1), (row - 1, 2), (0, col - 2), + (0, col - 1), (1, col - 1), (2, col - 1), (3, col - 1)], + [(row - 3, 0), (row - 2, 0), (row - 1, 0), (0, col - 4), + (0, col - 3), (0, col - 2), (0, col - 1), (1, col - 1)], + [(row - 3, 0), (row - 2, 0), (row - 1, 0), (0, col - 2), + (0, col - 1), (1, col - 1), (2, col - 1), (3, col - 1)], + [(row - 1, 0), (row - 1, col - 1), (0, col - 3), (0, col - 2), + (0, col - 1), (1, col - 3), (1, col - 2), (1, col - 1)], + + # "utah" places the 8 bits of a utah-shaped symbol character in ECC200 + [(row - 2, col -2), (row - 2, col -1), (row - 1, col - 2), (row - 1, col - 1), + (row - 1, col), (row, col - 2), (row, col - 1), (row, col)], + ][case][i] + module(array, nrow, ncol, x, y, bit(char, i + 1)) + return 1 + +def place_bits(data, nrow, ncol): + """fill an nrow x ncol array with the bits from the codewords in data.""" + # initialise and fill with -1"s (invalid value) + array = [[INVALID_BIT] * ncol for i in range(nrow)] + # Starting in the correct location for character #1, bit 8,... + char = 0 + row = 4 + col = 0 + while True: + + # first check for one of the special corner cases, then... + if (row == nrow) and (col == 0): + char += place_square(0, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow - 2) and (col == 0) and (ncol % 4): + char += place_square(1, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow - 2) and (col == 0) and (ncol % 8 == 4): + char += place_square(2, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow + 4) and (col == 2) and ((ncol % 8) == 0): + char += place_square(3, array, nrow, ncol, nrow, ncol, data[char]) + + # sweep upward diagonally, inserting successive characters,... + while (row >= 0) and (col < ncol): + if (row < nrow) and (col >= 0) and (array[row][col] == INVALID_BIT): + char += place_square(-1, array, nrow, ncol, row, col, data[char]) + row -= 2 + col += 2 + + row += 1 + col += 3 + + # & then sweep downward diagonally, inserting successive characters,... + while (row < nrow) and (col >= 0): + if (row >= 0) and (col < ncol) and (array[row][col] == INVALID_BIT): + char += place_square(-1, array, nrow, ncol, row, col, data[char]) + row += 2 + col -= 2 + + row += 3 + col += 1 + + # ... until the entire array is scanned + if not ((row < nrow) or (col < ncol)): + break + + # Lastly, if the lower righthand corner is untouched, fill in fixed pattern */ + if array[nrow - 1][ncol - 1] == INVALID_BIT: + array[nrow - 1][ncol - 2] = 0 + array[nrow - 1][ncol - 1] = 1 + array[nrow - 2][ncol - 1] = 0 + array[nrow - 2][ncol - 2] = 1 + + return array # return the array of 1"s and 0"s + +def add_finder_pattern(array, data_nrow, data_ncol, reg_row, reg_col): + # get the total size of the datamatrix + nrow = (data_nrow + 2) * reg_row + ncol = (data_ncol + 2) * reg_col + + datamatrix = [[0] * ncol for i in range(nrow)] # initialise and fill with 0"s + + for i in range(reg_col): # for each column of data regions + for j in range(nrow): + datamatrix[j][i * (data_ncol + 2)] = 1 # vertical black bar on left + datamatrix[j][i * (data_ncol + 2) + data_ncol + 1] = j % 2 # alternating blocks + + for i in range(reg_row): # for each row of data regions + for j in range(ncol): + datamatrix[i * (data_nrow + 2) + data_nrow + 1][j] = 1 # horizontal black bar at bottom + datamatrix[i * (data_nrow + 2)][j] = (j + 1) % 2 # alternating blocks + + for i in range(data_nrow * reg_row): + for j in range(data_ncol * reg_col): + # offset by 1, plus two for every addition block + dest_col = j + 1 + 2 * (j // data_ncol) + dest_row = i + 1 + 2 * (i // data_nrow) + + datamatrix[dest_row][dest_col] = array[i][j] # transfer from the plain bit array + + return datamatrix + +def get_valid_filename(s): + s = str(s).strip().replace(" ", "_") + return re.sub(r"(?u)[^-\w.]", "", s) + +def splitAt(string, length): + return ' '.join(string[i:i+length] for i in range(0,len(string),length)) + +class InventorySticker(inkex.Effect): + + def add_arguments(self, pars): + pars.add_argument("--tab") + pars.add_argument("--server_address", default="https://the.domain.de/items.csv") + pars.add_argument("--htuser", default="user") + pars.add_argument("--htpassword", default="password") + pars.add_argument("--sticker_ids", default="*") + pars.add_argument("--target_url", default="qwa.es") + pars.add_argument("--target_owner", default="Stadtfabrikanten e.V.") + pars.add_argument("--export_dir", default="/home/") + pars.add_argument("--flat_export", type=inkex.Boolean, default=False) + pars.add_argument("--preview", type=inkex.Boolean, default=False) + pars.add_argument("--export_svg", type=inkex.Boolean, default=True) + pars.add_argument("--export_png", type=inkex.Boolean, default=False) + pars.add_argument("--print_png", type=int, default=0) + pars.add_argument("--print_device", default="04f9:2044") + + def effect(self): + # Adjust the document view for the desired sticker size + root = self.svg.getElement("//svg:svg") + + subline_fontsize = 40 #px; one line of bottom text (id and owner) creates a box of that height + + #our DataMatrix has size 16x16, each cube is sized by 16x16px -> total size is 256x256px. We use 4px padding for all directions + DataMatrix_xy = 16 + DataMatrix_height = 16 * DataMatrix_xy + DataMatrix_width = DataMatrix_height + sticker_padding = 4 + sticker_height = DataMatrix_height + subline_fontsize + 3 * sticker_padding + sticker_width = 696 + + #configure font sizes and box heights to define how large the font size may be at maximum (to omit overflow) + objectNameMaxHeight = sticker_height - 2 * subline_fontsize - 4 * sticker_padding + objectNameMaxLines = 5 + objectNameFontSize = objectNameMaxHeight / objectNameMaxLines #px; generate main font size from lines and box size + + root.set("width", str(sticker_width) + "px") + root.set("height", str(sticker_height) + "px") + root.set("viewBox", "%f %f %f %f" % (0, 0, sticker_width, sticker_height)) + + #clean the document (make it blank) to avoid printing duplicated things + for node in self.document.xpath('//*', namespaces=inkex.NSS): + if node.TAG not in ('svg', 'defs', 'namedview'): + node.delete() + + #set the document units + self.document.getroot().find(inkex.addNS("namedview", "sodipodi")).set("inkscape:document-units", "px") + + # Download the recent inventory CSV file and parse line by line to create an inventory sticker + password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() + password_mgr.add_password(None, self.options.server_address, self.options.htuser, self.options.htpassword) + handler = urllib.request.HTTPBasicAuthHandler(password_mgr) + opener = urllib.request.build_opener(handler) + + try: + inventoryData = opener.open(self.options.server_address).read().decode("utf-8") + urllib.request.install_opener(opener) + + inventoryCSVParent = os.path.join(self.options.export_dir, "InventorySticker") + inventoryCSV = os.path.join(inventoryCSVParent, "inventory.csv") + + # To avoid messing with old stickers we remove the directory on Client before doing something new + shutil.rmtree(inventoryCSVParent, ignore_errors=True) #remove the output directory before doing new job + + # we are going to write the imported Server CSV file temporarily. Otherwise CSV reader seems to mess with the file if passed directly + if not os.path.exists(inventoryCSVParent): + os.mkdir(inventoryCSVParent) + with open(inventoryCSV, 'w', encoding="utf-8") as f: + f.write(inventoryData) + f.close() + + #parse sticker Ids from user input + if self.options.sticker_ids != "*": + sticker_ids = self.options.sticker_ids.split(",") + else: + sticker_ids = None + + with open(inventoryCSV, 'r', encoding="utf-8") as csv_file: + csv_reader = csv.reader(csv_file, delimiter=",") + + totalOutputs = 0 + for row in csv_reader: + internal_id = row[0] + doc_title = row[1] + sticker_id = row[2] + level = row[3] + zone = row[4] + + if sticker_ids is None or sticker_id in sticker_ids: + totalOutputs += 1 + #create new sub directories for each non-existent FabLab zone (if flat export is disabled) + if self.options.flat_export == False: + if not zone: + zoneDir = os.path.join(inventoryCSVParent, get_valid_filename("Keinem Bereich zugeordnet")) + else: + zoneDir = os.path.join(inventoryCSVParent, get_valid_filename(zone)) #remove invalid charaters from zone + if not os.path.exists(zoneDir): + os.mkdir(zoneDir) + else: + zoneDir = inventoryCSVParent #use top directory + + #Generate the recent sticker content + stickerGroup = self.document.getroot().add(inkex.Group(id="InventorySticker_Id" + sticker_id)) #make a new group at root level + DataMatrixStyle = inkex.Style({"stroke": "none", "stroke-width": "1", "fill": "#000000"}) + DataMatrixAttribs = {"style": str(DataMatrixStyle), "height": str(DataMatrix_xy) + "px", "width": str(DataMatrix_xy) + "px"} + + # 1 - create DataMatrix (create a 2d list corresponding to the 1"s and 0s of the DataMatrix) + encoded = self.encode(self.options.target_url + "/" + sticker_id) + DataMatrixGroup = stickerGroup.add(inkex.Group(id="DataMatrix_Id" + sticker_id)) #make a new group at root level + for x, y in self.render_data_matrix(encoded, DataMatrix_xy): + DataMatrixAttribs.update({"x": str(x + sticker_padding), "y": str(y + sticker_padding)}) + etree.SubElement(DataMatrixGroup, inkex.addNS("rect","svg"), DataMatrixAttribs) + + inline_size = sticker_width - DataMatrix_width - 3 * sticker_padding #remaining width for objects next to the DataMatrix + x_pos = DataMatrix_width + 2 * sticker_padding + + # 2 - Add Object Name Text + objectName = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(objectNameFontSize) + "px", + "x": str(x_pos) + "px", + #"xml:space": "preserve", #we cannot add this here because InkScape throws an error + "y": str(objectNameFontSize) + "px", + "text-align" : "left", + "text-anchor": "left", + "vertical-align" : "bottom", + #style: inline-size required for text wrapping inside box; letter spacing is required to remove the additional whitespaces. The letter spacing depends to the selected font family (Miso) + "style": str(inkex.Style({"fill": "#000000", "writing-mode": "horizontal-tb", "inline-size": str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "bold", "letter-spacing": "-3.66px"})) + } + ) + objectName.set("id", "objectName_Id" + sticker_id) + objectName.set("xml:space", "preserve") #so we add it here instead .. if multiple whitespaces in text are coming after each other just render them (preserve!) + objectNameTextSpan = etree.SubElement(objectName, inkex.addNS("tspan", "svg"), {}) + objectNameTextSpan.text = splitAt(doc_title, 1) #add 1 whitespace after each chacter. So we can simulate a in-word line break (break by char instead by word) + + # 3 - Add Object Id Text - use the same position but revert text anchors/align + objectId = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(subline_fontsize) + "px", + "x": str(sticker_padding) + "px", + "y": "30px", + "transform": "translate(0," + str(sticker_height - subline_fontsize) + ")", + "text-align" : "left", + "text-anchor": "left", + "vertical-align" : "bottom", + "style": str(inkex.Style({"fill": "#000000", "inline-size":str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "bold"})) #inline-size required for text wrapping + } + ) + objectId.set("id", "objectId_Id" + sticker_id) + objectIdTextSpan = etree.SubElement(objectId, inkex.addNS("tspan", "svg"), {}) + objectIdTextSpan.text = "Thing #" + sticker_id + + # 4 - Add Owner Text + owner = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(subline_fontsize) + "px", + "x": str(x_pos) + "px", + "y": "30px", + "transform": "translate(0," + str(sticker_height - subline_fontsize) + ")", + "text-align" : "right", + "text-anchor": "right", + "vertical-align" : "bottom", + "style": str(inkex.Style({"fill": "#000000", "inline-size":str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "300"})) #inline-size required for text wrapping + } + ) + owner.set("id", "owner_Id" + sticker_id) + ownerTextSpan = etree.SubElement(owner, inkex.addNS("tspan", "svg"), {}) + ownerTextSpan.text = self.options.target_owner + + # 5 - Add Level Text + levelText = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(subline_fontsize) + "px", + "x": str(x_pos) + "px", + "y": "30px", + "transform": "translate(0," + str(sticker_height - subline_fontsize - subline_fontsize) + ")", + "text-align" : "right", + "text-anchor": "right", + "vertical-align" : "bottom", + "style": str(inkex.Style({"fill": "#000000", "inline-size":str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "bold"})) #inline-size required for text wrapping + } + ) + levelText.set("id", "level_Id" + sticker_id) + levelTextTextSpan = etree.SubElement(levelText, inkex.addNS("tspan", "svg"), {}) + levelTextTextSpan.text = level + + # 6 - Add horizontal divider line + line_thickness = 2 #px + line_x_pos = 350 #px; start of the line (left coord) + line_length = sticker_width - line_x_pos + divider = etree.SubElement(stickerGroup, + inkex.addNS("path", "svg"), + { + "d": "m " + str(line_x_pos) + "," + str(sticker_height - subline_fontsize - subline_fontsize) + " h " + str(line_length) , + "style": str(inkex.Style({"fill": "none", "stroke": "#000000", "stroke-width": str(line_thickness) + "px", "stroke-linecap": "butt", "stroke-linejoin":"miter", "stroke-opacity": "1"})) #inline-size required for text wrapping + } + ) + divider.set("id", "divider_Id" + sticker_id) + + if self.options.preview == False: + export_file_name = sticker_id + "_" + get_valid_filename(doc_title) + export_file_path = os.path.join(zoneDir, export_file_name) + + #"Export" as SVG by just copying the recent SVG document to the target directory. We need to remove special characters to have valid file names on Windows/Linux + export_file_svg = open(export_file_path + ".svg", "w", encoding="utf-8") + export_file_svg.write(str(etree.tostring(self.document), "utf-8")) + export_file_svg.close() + + if self.options.export_png == False and self.options.export_svg == False: + inkex.errormsg("Nothing to export. Generating preview only ...") + break + + if self.options.export_png == True: #we need to generate SVG before to get PNG. But if user selected PNG only we need to remove SVG afterwards + #Make PNG from SVG (slow because each file is picked up separately. Takes about 10 minutes for 600 files + inkscape(export_file_path + ".svg", actions="export-dpi:96;export-background:white;export-filename:{file_name};export-do;FileClose".format(file_name=export_file_path + ".png")) + + #fix for "usb.core.USBError: [Errno 13] Access denied (insufficient permissions)" + #echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2044", MODE="666"' > /etc/udev/rules.d/99-garmin.rules && sudo udevadm trigger + if self.options.print_png > 0: + if self.options.export_png == False: + inkex.errormsg("No file output for printing. Please set 'Export PNG' to true first.") + else: + for x in range(self.options.print_png): + command = "brother_ql -m QL-720NW --backend pyusb --printer usb://" + self.options.print_device + " print -l 62 --600dpi -r auto " + export_file_path + ".png" + p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) #forr Windows: shell=False + stdout, stderr = p.communicate() + p.wait() + if p.returncode != 0: + std_out = stdout.decode('utf-8') + std_err = stderr.decode('utf-8') + if std_err.endswith("ValueError: Device not found\n") is True: + self.msg("Printer device not found or offline. Check for power, cables and entered printer interface ID") + else: + inkex.errormsg("brother_ql returned errors:\nError code {:d}\n{}\n{}".format(p.returncode, std_out, std_err)) + + if self.options.export_svg != True: #If user selected PNG only we need to remove SVG again + os.remove(export_file_path + ".svg") + + self.document.getroot().remove(stickerGroup) #remove the stickerGroup again + else: #create preview by just breaking the for loop without executing remove(stickerGroup) + break + csv_file.close() + if totalOutputs == 0: + self.msg("No output was generated. Check if your entered IDs are valid!") + except Exception as e: + inkex.errormsg(e) + #inkex.errormsg("Wrong inventory.csv URL or invalid credentials for Basic Auth") + + # parameters for the selected datamatrix size + # drow number of rows in each data region + # dcol number of cols in each data region + # reg_row number of rows of data regions + # reg_col number of cols of data regions + # nd number of data codewords per reed-solomon block + # nc number of ECC codewords per reed-solomon block + # inter number of interleaved Reed-Solomon blocks + def encode(self, text, nrow = 16, ncol = 16, data_nrow = 14, data_ncol = 14, reg_row = 1, reg_col = 1, nd = 12, nc = 12, inter = 1): + """ + Take an input string and convert it to a sequence (or sequences) + of codewords as specified in ISO/IEC 16022:2006 (section 5.2.3) + """ + # generate the codewords including padding and ECC + codewords = get_codewords(text, nd, nc, inter, nrow == 144) + + # break up into separate arrays if more than one DataMatrix is needed + module_arrays = [] + for codeword_stream in codewords: # for each datamatrix + # place the codewords" bits across the array as modules + bit_array = place_bits(codeword_stream, data_nrow * reg_row, data_ncol * reg_col) + # add finder patterns around the modules + module_arrays.append(add_finder_pattern(bit_array, data_nrow, data_ncol, reg_row, reg_col)) + + return module_arrays + + def render_data_matrix(self, module_arrays, size): + """turn a 2D array of 1"s and 0"s into a set of black squares""" + spacing = 16 * size * 1.5 + for i, line in enumerate(module_arrays): + height = len(line) + width = len(line[0]) + + for y in range(height): # loop over all the modules in the datamatrix + for x in range(width): + if line[y][x] == 1: # A binary 1 is a filled square + yield (x * size + i * spacing, y * size) + elif line[y][x] == INVALID_BIT: # we have an invalid bit value + inkex.errormsg("Invalid bit value, {}!".format(line[y][x])) + +if __name__ == "__main__": + InventorySticker().run() diff --git a/extensions/fablabchemnitz/inventory_sticker/meta.json b/extensions/fablabchemnitz/inventory_sticker/meta.json new file mode 100644 index 0000000..4c92eab --- /dev/null +++ b/extensions/fablabchemnitz/inventory_sticker/meta.json @@ -0,0 +1,20 @@ +[ + { + "name": "Inventory Sticker", + "id": "fablabchemnitz.de.inventory_sticker", + "path": "inventory_sticker", + "dependent_extensions": null, + "original_name": "Inventory Sticker", + "original_id": "fablabchemnitz.de.inventory_sticker", + "license": "GNU GPL v3", + "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE", + "comment": "", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/inventory_sticker", + "fork_url": null, + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Inventory+Sticker", + "inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85inventory-sticker", + "main_authors": [ + "github.com/eridur-de" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.inx b/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.inx new file mode 100644 index 0000000..fbb81d1 --- /dev/null +++ b/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.inx @@ -0,0 +1,79 @@ + + + Lasercut Jigsaw + fablabchemnitz.de.lasercut_jigsaw + + + + 4278190335 + 65535 + + + + + + + + 100.0 + 80.0 + 5.0 + + + + + + + + false + 20.0 + 5.0 + + + + + + 5 + 4 + + + + 0.5 + 0.4 + false + 10 + true + 12345 + false + 0 + + + + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.py b/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.py new file mode 100644 index 0000000..5725ce9 --- /dev/null +++ b/extensions/fablabchemnitz/lasercut_jigsaw/lasercut_jigsaw.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +''' +Copyright (C) 2011 Mark Schafer + +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 +''' + +# Build a Jigsaw puzzle for Lasercutting. +# User defines: +# - dimensions, +# - number of pieces in X and Y, +# - notch size, +# - random amount of perturbation for uniqueness, +# - border and rounding for border and inner corners +# - random or random seed for repeats + +### 0.1 make basic jigsaw for lasercut - March 2011 +### 0.2 add random seed so repeatable, add pieces for manual booleans - May 2011 +### 0.3 add some no-knob edges - June 2019 + +### Todo +# add option to cut pieces: +# - taking two rows(cols) at a time - reverse the second one and concat on end - add z to close +# - taking a row and a col - do intersect = piece. + +__version__ = "0.3" + +import sys, math, random, copy +from lxml import etree +import inkex +from inkex import Path, CubicSuperPath, Color +from inkex.command import inkscape + +def dirtyFormat(path): + return str(path).replace('[','').replace(']','').replace(',','').replace('\'','') + +def randomize(x_y, radius, norm=True, absolute=False): + """ return x,y moved by a random amount inside a radius. + use uniform distribution unless + - norm = True - then use a normal distribution + If absolute is true - ensure random is only added to x,y """ + # if norm: + # r = abs(random.normalvariate(0.0,0.5*radius)) + # else: + # r = random.uniform(0.0,radius) + x, y = x_y + a = random.uniform(0.0,2*math.pi) + x += math.cos(a)*radius + y += math.sin(a)*radius + if absolute: + x = abs(x) + y = abs(y) + return [x, y] + +def add_rounded_rectangle(startx, starty, radius, width, height, style, name, parent, mask=False): + line_path = [['M', [startx, starty+radius]]] + if radius > 0.0: # rounded corners + line_path.append(['c', [0, -radius/2, radius/2, -radius, radius, -radius]]) + if mask == "Below": + line_path.append(['m', [width-2*radius, 0,]]) + else: + line_path.append(['c', [radius/2, 0, width-2*radius-radius/2, 0, width-2*radius,0 ]]) # top line + line_path.append(['c', [radius/2, 0, radius, radius/2, radius, radius]]) + line_path.append(['c', [0, radius/2, 0, height-2*radius-radius/2, 0, height-2*radius]]) # RHS line + line_path.append(['c', [0, radius/2, -radius/2, radius, -radius, radius]]) + line_path.append(['c', [-radius/2,0, -width+2*radius+radius/2,0, -width+2*radius,0]]) # bottom line + line_path.append(['c', [-radius/2, 0, -radius, -radius/2, -radius, -radius]]) + if mask == "Right": + line_path.append(['m', [0, height]]) + else: + line_path.append(['c', [0, -radius/2, 0, -height+2*radius+radius/2, 0, -height+2*radius]]) # LHS line + else: # square corners + if mask == "Below": + line_path.append(['m', [width, 0]]) + line_path.append(['l', [0, height, -width, 0, 0, -height]]) + elif mask == "Right": + line_path.append(['l', [width, 0, 0, height, -width, 0,]]) + else: # separate + line_path.append(['l', [width, 0, 0, height, -width, 0, 0, -height]]) + # + #sys.stderr.write("%s\n"% line_path) + attribs = {'style':str(inkex.Style(style)), inkex.addNS('label','inkscape'):name, 'd':dirtyFormat(line_path)} + #sys.stderr.write("%s\n"% attribs) + etree.SubElement(parent, inkex.addNS('path','svg'), attribs ) + +###---------------------- +### all for intersection from http://www.kevlindev.com/gui/index.htm + +def get_derivative(polynomial): + deriv = [] + for i in range(len(polynomial)): + deriv.append(i* polynomial[i]) + return deriv + +class LasercutJigsaw(inkex.EffectExtension): + + def add_arguments(self, pars): + # General settings + pars.add_argument("--tab") + + #Style + pars.add_argument("--color_border", type=Color, default='4278190335', help="Border color") + pars.add_argument("--color_jigsaw", type=Color, default='65535', help="Jigsaw lines color") + + #Dimensions + pars.add_argument("--sizetype", default="boxsize") + pars.add_argument("--width", type=float, default=50.0) + pars.add_argument("--height", type=float, default=30.0) + pars.add_argument("--innerradius", type=float, default=5.0, help="0 implies square corners") + pars.add_argument("--units", default="cm", help="The unit of the box dimensions") + pars.add_argument("--border", type=inkex.Boolean, default=False, help="Add Outer Surround") + pars.add_argument("--borderwidth", type=float, default=10.0, help="Size of external surrounding border.") + pars.add_argument("--outerradius", type=float, default=5.0, help="0 implies square corners") + pars.add_argument("--pack", default="Below", help="Where to place backing piece on page") + pars.add_argument("--pieces_W", type=int, default=11, help="How many pieces across") + pars.add_argument("--pieces_H", type=int, default=11, help="How many pieces down") + + #Notches + pars.add_argument("--notch_percent", type=float, default=0.0, help="Notch relative size. 0 to 1. 0.15 is good") + pars.add_argument("--rand", type=float, default=0.1, help="Amount to perturb the basic piece grid.") + pars.add_argument("--noknob_frequency", type=float, default=10, help="Percentage of smooth-sided edges.") + pars.add_argument("--smooth_edges", type=inkex.Boolean, default=False, help="Allow pieces with smooth edges.") + pars.add_argument("--use_seed", type=inkex.Boolean, default=False, help="Use the kerf value as the drawn line width") + pars.add_argument("--seed", type=int, default=12345, help="Random seed for repeatability") + pars.add_argument("--pieces", type=inkex.Boolean, default=False, help="Create separated pieces") + pars.add_argument("--shift", type=float, default=0.0, help="Shifting for each piece (%)") + + def add_jigsaw_horiz_line(self, startx, starty, stepx, steps, width, style, name, parent): + """ complex version All C smooth + - get ctrl pt offset and use on both sides of each node (negate for smooth)""" + line_path = [] + # starts with an M - then C with first point same as M = smooth (also last point still in C but doubled) + line_path.append(['M', [startx, starty]]) + clist = [startx, starty] # duplicate 1st point so its smooth + for i in range(1,steps+1): + flip = 1 + if random.uniform(0.0,1.0) < 0.5: + flip = -1 + do_smooth = False + if self.smooth_edges: + if random.uniform(0.0,100.0) < self.noknob_frequency: + do_smooth = True + if do_smooth: + pt1 = randomize((startx+i*stepx/2+stepx/2*(i-1), starty), self.random_radius/3, True) + rand1 = randomize((0, 0), self.random_radius/4, True, True) + # up to pt1 + ctrl1 = (-self.notch_step*1.5, self.notch_step*1.5) + clist.extend([pt1[0]+ctrl1[0]-rand1[0], pt1[1]-ctrl1[1]*flip+rand1[1]*flip]) + clist.extend(pt1) + # last ctrl point for next step + clist.extend([pt1[0]-ctrl1[0]+rand1[0], pt1[1]+ctrl1[1]*flip-rand1[1]*flip]) + else: + pt1 = randomize((startx-self.notch_step+i*stepx/2+stepx/2*(i-1), starty+self.notch_step/4*flip), self.random_radius/3, True) + pt2 = randomize((startx-self.notch_step+i*stepx/2+stepx/2*(i-1), starty-self.notch_step*flip), self.random_radius/3, True) + # pt3 is foor tip of the notch - required ? + pt4 = randomize((startx+self.notch_step+i*stepx/2+stepx/2*(i-1), starty-self.notch_step*flip), self.random_radius/3, True) #mirror of 2 + pt5 = randomize((startx+self.notch_step+i*stepx/2+stepx/2*(i-1), starty+self.notch_step/4*flip), self.random_radius/3, True) # mirror of pt1 + # Create random local value for x,y of handle - then reflect to enforce smoothness + rand1 = randomize((0, 0), self.random_radius/4, True, True) + rand2 = randomize((0, 0), self.random_radius/4, True, True) + rand4 = randomize((0, 0), self.random_radius/4, True, True) + rand5 = randomize((0, 0), self.random_radius/4, True, True) + # up to pt1 + #ctrl1_2 = (startx+i*stepx/2+(i-1)*stepx/2, starty-self.notch_step/3) + ctrl1 = (self.notch_step/1.2, -self.notch_step/3) + clist.extend([pt1[0]-ctrl1[0]-rand1[0], pt1[1]-ctrl1[1]*flip+rand1[1]*flip]) + clist.extend(pt1) + # up to pt2 + clist.extend([pt1[0]+ctrl1[0]+rand1[0], pt1[1]+ctrl1[1]*flip-rand1[1]*flip]) + ctrl2 = (0, -self.notch_step/1.2) + clist.extend([pt2[0]+ctrl2[0]-rand2[0], pt2[1]-ctrl2[1]*flip+rand2[1]*flip]) + clist.extend(pt2) + # up to pt4 + clist.extend([pt2[0]-ctrl2[0]+rand2[0], pt2[1]+ctrl2[1]*flip-rand2[1]*flip]) + ctrl4 = (0, self.notch_step/1.2) + clist.extend([pt4[0]+ctrl4[0]-rand4[0], pt4[1]-ctrl4[1]*flip+rand4[1]*flip]) + clist.extend(pt4) + # up to pt5 + clist.extend([pt4[0]-ctrl4[0]+rand4[0], pt4[1]+ctrl4[1]*flip-rand4[1]*flip]) + ctrl5 = (self.notch_step/1.2, self.notch_step/3) + clist.extend([pt5[0]-ctrl5[0]+rand5[0], pt5[1]-ctrl5[1]*flip-rand5[1]*flip]) + clist.extend(pt5) + # last ctrl point for next step + clist.extend([pt5[0]+ctrl5[0]-rand5[0], pt5[1]+ctrl5[1]*flip+rand5[1]*flip]) + # + clist.extend([width, starty, width, starty]) # doubled up at end for smooth curve + line_path.append(['C',clist]) + borderLineStyle = str(inkex.Style(style)) + attribs = { 'style':borderLineStyle, 'id':name, 'd':dirtyFormat(line_path)} + etree.SubElement(parent, inkex.addNS('path','svg'), attribs ) + + def create_horiz_blocks(self, group, gridy, style): + path = lastpath = 0 + blocks = [] + count = 0 + for node in gridy.iterchildren(): + if node.tag == inkex.addNS('path','svg'): # which they ALL should because we just made them + path = CubicSuperPath(node.get('d')) # turn it into a global C style SVG path + #sys.stderr.write("count: %d\n"% count) + if count == 0: # first one so use the top border + spath = node.get('d') # work on string instead of cubicpath + lastpoint = spath.split()[-2:] + lastx = float(lastpoint[0]) + lasty = float(lastpoint[1]) + #sys.stderr.write("lastpoint: %s\n"% lastpoint) + spath += ' %f %f %f %f %f %f' % (lastx,lasty-self.inner_radius, lastx,1.5*self.inner_radius, lastx,self.inner_radius) + spath += ' %f %f %f %f %f %f' % (self.width,self.inner_radius/2, self.width-self.inner_radius/2,0, self.width-self.inner_radius,0) + spath += ' %f %f %f %f %f %f' % (self.width-self.inner_radius/2,0, 1.5*self.inner_radius,0, self.inner_radius, 0) + spath += ' %f %f %f %f %f %f' % (self.inner_radius/2, 0, 0,self.inner_radius/2, 0,self.inner_radius) + spath += 'z' + #sys.stderr.write("spath: %s\n"% spath) + # + name = "RowPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':spath } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + else: # internal line - concat a reversed version with the last one + thispath = copy.deepcopy(path) + for i in range(len(thispath[0])): # reverse the internal C pairs + thispath[0][i].reverse() + thispath[0].reverse() # reverse the entire line + lastpath[0].extend(thispath[0]) # append + name = "RowPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':dirtyFormat(lastpath) } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + n.set('d', n.get('d')+'z') # close it + # + count += 1 + lastpath = path + # do the last row + spath = node.get('d') # work on string instead of cubicpath + lastpoint = spath.split()[-2:] + lastx = float(lastpoint[0]) + lasty = float(lastpoint[1]) + #sys.stderr.write("lastpoint: %s\n"% lastpoint) + spath += ' %f %f %f %f %f %f' % (lastx,lasty+self.inner_radius, lastx,self.height-1.5*self.inner_radius, lastx,self.height-self.inner_radius) + spath += ' %f %f %f %f %f %f' % (self.width,self.height-self.inner_radius/2, self.width-self.inner_radius/2,self.height, self.width-self.inner_radius,self.height) + spath += ' %f %f %f %f %f %f' % (self.width-self.inner_radius/2,self.height, 1.5*self.inner_radius,self.height, self.inner_radius, self.height) + spath += ' %f %f %f %f %f %f' % (self.inner_radius/2, self.height, 0,self.height-self.inner_radius/2, 0,self.height-self.inner_radius) + spath += 'z' + # + name = "RowPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':spath } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + # + return(blocks) + + + def create_vert_blocks(self, group, gridx, style): + path = lastpath = 0 + blocks = [] + count = 0 + for node in gridx.iterchildren(): + if node.tag == inkex.addNS('path','svg'): # which they ALL should because we just made them + path = CubicSuperPath(node.get('d')) # turn it into a global C style SVG path + #sys.stderr.write("count: %d\n"% count) + if count == 0: # first one so use the right border + spath = node.get('d') # work on string instead of cubicpath + lastpoint = spath.split()[-2:] + lastx = float(lastpoint[0]) + lasty = float(lastpoint[1]) + #sys.stderr.write("lastpoint: %s\n"% lastpoint) + spath += ' %f %f %f %f %f %f' % (lastx+self.inner_radius/2,lasty, self.width-1.5*self.inner_radius,lasty, self.width-self.inner_radius, lasty) + spath += ' %f %f %f %f %f %f' % (self.width-self.inner_radius/2,lasty, self.width,self.height-self.inner_radius/2, self.width,self.height-self.inner_radius) + spath += ' %f %f %f %f %f %f' % (self.width,self.height-1.5*self.inner_radius, self.width, 1.5*self.inner_radius, self.width,self.inner_radius) + spath += ' %f %f %f %f %f %f' % (self.width,self.inner_radius/2, self.width-self.inner_radius/2,0, self.width-self.inner_radius,0) + spath += 'z' + #sys.stderr.write("spath: %s\n"% spath) + # + name = "ColPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':spath } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + else: # internal line - concat a reversed version with the last one + thispath = copy.deepcopy(path) + for i in range(len(thispath[0])): # reverse the internal C pairs + thispath[0][i].reverse() + thispath[0].reverse() # reverse the entire line + lastpath[0].extend(thispath[0]) # append + name = "ColPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':dirtyFormat(lastpath) } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + n.set('d', n.get('d')+'z') # close it + # + count += 1 + lastpath = path + # do the last one (LHS) + spath = node.get('d') # work on string instead of cubicpath + lastpoint = spath.split()[-2:] + lastx = float(lastpoint[0]) + lasty = float(lastpoint[1]) + #sys.stderr.write("lastpoint: %s\n"% lastpoint) + spath += ' %f %f %f %f %f %f' % (lastx-self.inner_radius,lasty, 1.5*self.inner_radius, lasty, self.inner_radius,lasty) + spath += ' %f %f %f %f %f %f' % (self.inner_radius/2,lasty, 0,lasty-self.inner_radius/2, 0,lasty-self.inner_radius) + spath += ' %f %f %f %f %f %f' % (0,lasty-1.5*self.inner_radius, 0,1.5*self.inner_radius, 0,self.inner_radius) + spath += ' %f %f %f %f %f %f' % (self.inner_radius/2,0, self.inner_radius,0, 1.5*self.inner_radius, 0) + spath += 'z' + # + name = "ColPieces_%d" % (count) + attribs = { 'style':style, 'id':name, 'd':spath } + n = etree.SubElement(group, inkex.addNS('path','svg'), attribs ) + blocks.append(n) # for direct traversal later + # + return(blocks) + + + def create_pieces(self, jigsaw, gridx, gridy): + """ Loop through each row """ + # Treat outer edge carefully as border runs around. So special code the edges + # Internal lines should be in pairs - with second line reversed and appended to first. Close with a 'z' + # Create new group + g_attribs = {inkex.addNS('label','inkscape'):'JigsawPieces:X' + \ + str( self.pieces_W )+':Y'+str( self.pieces_H ) } + jigsaw_pieces = etree.SubElement(jigsaw, 'g', g_attribs) + jigsaw_pieces_id = self.svg.get_unique_id("pieces-") + jigsaw_pieces.attrib['id'] = jigsaw_pieces_id + borderLineStyle = str(inkex.Style(self.borderLineStyle)) + + xblocks = self.create_horiz_blocks(jigsaw_pieces, gridy, borderLineStyle) + yblocks = self.create_vert_blocks(jigsaw_pieces, gridx, borderLineStyle) + + # for each xblock intersect it with each Y block + puzzlePartNo = 1 + allPathPairsToIntersect = [] + allPathsToDelete = [] + + for x in range(len(xblocks)): + for y in range(len(yblocks)): + allPathPairsToIntersect.append([copy.copy(xblocks[x]), copy.copy(yblocks[y])]) + allPathsToDelete.append(xblocks[x]) + allPathsToDelete.append(yblocks[y]) + + for pair in allPathPairsToIntersect: + pair[0].attrib['id'] = str(puzzlePartNo) + "_X" + pair[1].attrib['id'] = str(puzzlePartNo) + "_Y" + xId = pair[0].get('id') + yId = pair[1].get('id') + #self.msg("intersecting {} with {}".format(xId, yId)) + puzzlePartNo += 1 + jigsaw_pieces.append(pair[0]) + jigsaw_pieces.append(pair[1]) + + for pathToDelete in allPathsToDelete: + pathToDelete.delete() + + actions_list = [] + for pair in allPathPairsToIntersect: + actions_list.append("select-by-id:{}".format(pair[0].attrib['id'])) + actions_list.append("select-by-id:{}".format(pair[1].attrib['id'])) + actions_list.append("path-intersection") + actions_list.append("select-clear") + + #self.msg(actions_list) + + #workaround to fix it (we use export to tempfile instead processing and saving again) + tempfile = self.options.input_file + "-intersected.svg" + with open(tempfile, 'wb') as fp: + fp.write(self.svg.tostring()) + actions_list.append("export-type:svg") + actions_list.append("export-filename:{}".format(tempfile)) + actions_list.append("export-do") + actions = ";".join(actions_list) + #self.msg(actions) + cli_output = inkscape(tempfile, actions=actions) #process recent file + if len(cli_output) > 0: + self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:") + self.msg(cli_output) + + # replace current document with content of temp copy file + self.document = inkex.load_svg(tempfile) + # update self.svg + self.svg = self.document.getroot() + + row = 1 + col = 1 + offsetW = self.options.shift / 100 * (self.options.width / self.options.pieces_W) + offsetH = self.options.shift / 100 * (self.options.height / self.options.pieces_H) + for jigsaw_piece in self.svg.getElementById(jigsaw_pieces_id).getchildren(): + jigsaw_piece.apply_transform() + jigsaw_piece.set('transform', 'translate(%f,%f)' % (-col * offsetH, 0)) + jigsaw_piece.apply_transform() + jigsaw_piece.set('transform', 'translate(%f,%f)' % (0, row * offsetW)) + jigsaw_piece.apply_transform() + currentPiece = int(jigsaw_piece.get('id').split('_')[0]) + #self.msg("piece {} zeile {} Spalte {}".format(currentPiece, row, col)) + if currentPiece % self.options.pieces_W == 0: + row += 1 + col -= self.options.pieces_W + col += 1 + return jigsaw_pieces_id + + def effect(self): + + # internal useful variables + self.stroke_width =str(self.svg.unittouu("1px")) # default for visiblity + self.borderLineStyle = {'stroke': self.options.color_border, 'fill': 'none', 'stroke-width': self.stroke_width, + 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter'} + self.jigsawLineStyle = {'stroke': self.options.color_jigsaw, 'fill': 'none', 'stroke-width': self.stroke_width, + 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter'} + + + # document dimensions (for centering) + docW = self.svg.unittouu(self.document.getroot().get('width')) + docH = self.svg.unittouu(self.document.getroot().get('height')) + + # extract fields from UI + self.width = self.svg.unittouu( str(self.options.width) + self.options.units ) + self.height = self.svg.unittouu( str(self.options.height) + self.options.units ) + self.pieces_W = self.options.pieces_W + self.pieces_H = self.options.pieces_H + + if self.options.sizetype == "partsize": + self.width = self.width * self.pieces_W + self.height = self.height * self.pieces_H + + average_block = (self.width/self.pieces_W + self.height/self.pieces_H) / 2 + self.notch_step = average_block * self.options.notch_percent / 3 # 3 = a useful notch size factor + self.smooth_edges = self.options.smooth_edges + self.noknob_frequency = self.options.noknob_frequency + self.random_radius = self.options.rand * average_block / 5 # 5 = a useful range factor + self.inner_radius = self.options.innerradius + if self.inner_radius < 0.01: self.inner_radius = 0.0 # snap to 0 for UI error when setting spinner to 0.0 + self.border = self.options.border + self.borderwidth = self.options.borderwidth + self.outer_radius = self.options.outerradius + if self.outer_radius < 0.01: self.outer_radius = 0.0 # snap to 0 for UI error when setting spinner to 0.0 + self.pack = self.options.pack + # pieces + self.pieces = self.options.pieces + # random function + if not self.options.use_seed: + random.seed(self.options.seed) + + # + # set up the main object in the current layer - group gridlines + g_attribs = {inkex.addNS('label','inkscape'):'Jigsaw:X' + str(self.pieces_W )+':Y'+str(self.pieces_H) + "={}Pcs)".format(self.pieces_W * self.pieces_H)} + jigsaw_group = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs) + #Group for X grid + g_attribs = {inkex.addNS('label','inkscape'):'X_Gridlines'} + gridx = etree.SubElement(jigsaw_group, 'g', g_attribs) + #Group for Y grid + g_attribs = {inkex.addNS('label','inkscape'):'Y_Gridlines'} + gridy = etree.SubElement(jigsaw_group, 'g', g_attribs) + + # Draw the Border + add_rounded_rectangle(0,0, self.inner_radius, self.width, self.height, self.borderLineStyle, 'innerborder', jigsaw_group) + # Do the Border + if self.border: + add_rounded_rectangle(-self.borderwidth,-self.borderwidth, self.outer_radius, self.borderwidth*2+self.width, + self.borderwidth*2+self.height, self.borderLineStyle, 'outerborder', jigsaw_group) + # make a second copy below the jigsaw for the cutout BG + if self.pack == "Below": + add_rounded_rectangle(-self.borderwidth,self.borderwidth+ self.height, self.outer_radius, self.borderwidth*2+self.width, + self.borderwidth*2+self.height, self.borderLineStyle, 'BG', jigsaw_group, self.pack) + elif self.pack == "Right": + add_rounded_rectangle(self.width+self.borderwidth,-self.borderwidth, self.outer_radius, self.borderwidth*2+self.width, + self.borderwidth*2+self.height, self.borderLineStyle, 'BG', jigsaw_group, self.pack) + else: # Separate + add_rounded_rectangle(self.width+self.borderwidth*2,-self.borderwidth, self.outer_radius, self.borderwidth*2+self.width, + self.borderwidth*2+self.height, self.borderLineStyle, 'BG', jigsaw_group) + + # Step through the Grid + Xstep = self.width / (self.pieces_W) + Ystep = self.height / (self.pieces_H) + # Draw Horizontal lines on Y step with Xstep notches + for i in range(1, self.pieces_H): + self.add_jigsaw_horiz_line(0, Ystep*i, Xstep, self.pieces_W, self.width, self.jigsawLineStyle, 'YDiv'+str(i), gridy) + # Draw Vertical lines on X step with Ystep notches + for i in range(1, self.pieces_W): + self.add_jigsaw_horiz_line(0, Xstep*i, Ystep, self.pieces_H, self.height, self.jigsawLineStyle, 'XDiv'+str(i), gridx) + # Rotate lines into pos + # actualy transform can have multiple transforms in it e.g. 'translate(10,10) rotate(10)' + for node in gridx.iterchildren(): + if node.tag == inkex.addNS('path','svg'): + node.set('transform', 'translate(%f,%f) rotate(90)' % (self.width, 0)) + node.apply_transform() + # center the jigsaw + jigsaw_group.set('transform', 'translate(%f,%f)' % ( (docW-self.width)/2, (docH-self.height)/2 ) ) + + #inkex.utils.debug("Your puzzle consists out of {} pieces.".format(self.pieces_W * self.pieces_H)) + + # pieces + if self.pieces: + gridx.delete() #delete the previous x generated stuff because we have single pieces instead! + gridy.delete() #delete the previous y generated stuff because we have single pieces instead! + jigsaw_group.getchildren()[0].delete() #delete inner border + jigsaw_pieces_id = self.create_pieces(jigsaw_group, gridx,gridy) + for jigsaw_piece in self.svg.getElementById(jigsaw_pieces_id).getchildren(): + jigsaw_piece.attrib['id'] = jigsaw_pieces_id + "_" + jigsaw_piece.attrib['id'] + +if __name__ == '__main__': + LasercutJigsaw().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/lasercut_jigsaw/meta.json b/extensions/fablabchemnitz/lasercut_jigsaw/meta.json new file mode 100644 index 0000000..87b74d0 --- /dev/null +++ b/extensions/fablabchemnitz/lasercut_jigsaw/meta.json @@ -0,0 +1,25 @@ +[ + { + "name": "Lasercut Jigsaw", + "id": "fablabchemnitz.de.lasercut_jigsaw", + "path": "lasercut_jigsaw", + "dependent_extensions": null, + "original_name": "Lasercut Jigsaw", + "original_id": "org.inkscape.LasercutJigsa", + "license": "GNU GPL v2", + "license_url": "https://github.com/Neon22/inkscape-jigsaw/blob/master/LICENSE", + "comment": "", + "source_url": "https://stadtfabrikanten.org/display/IFM/Lasercut+Jigsaw", + "fork_url": "https://github.com/Neon22/inkscape-jigsaw", + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Lasercut+Jigsaw", + "inkscape_gallery_url": null, + "main_authors": [ + "github.com/Neon22", + "github.com/jonadem", + "github.com/speleo3", + "github.com/LynNor1", + "github.com/roeschter", + "github.com/vmario89" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/line_animator/meta.json b/extensions/fablabchemnitz/line_animator/meta.json index 945c914..2ad092a 100644 --- a/extensions/fablabchemnitz/line_animator/meta.json +++ b/extensions/fablabchemnitz/line_animator/meta.json @@ -9,13 +9,13 @@ "license": "GNU GPL v2", "license_url": "https://gitlab.com/Moini/ink_line_animator/-/blob/master/LICENSE", "comment": "", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/line_animator", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/line_animator", "fork_url": "https://gitlab.com/Moini/ink_line_animator", "documentation_url": "", "inkscape_gallery_url": null, "main_authors": [ "gitlab.com/Moini", - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/paperfold/meta.json b/extensions/fablabchemnitz/paperfold/meta.json new file mode 100644 index 0000000..5ae96ed --- /dev/null +++ b/extensions/fablabchemnitz/paperfold/meta.json @@ -0,0 +1,21 @@ +[ + { + "name": "Paperfold", + "id": "fablabchemnitz.de.paperfold", + "path": "paperfold", + "dependent_extensions": null, + "original_name": "Paperfold", + "original_id": "fablabchemnitz.de.paperfold", + "license": "GNU GPL v3", + "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE", + "comment": "Written by Mario Voigt, based on https://github.com/felixfeliz/paperfoldmodels", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/paperfold", + "fork_url": null, + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Paperfold", + "inkscape_gallery_url": "https://inkscape.org/~MarioVoigt/%E2%98%85paperfold", + "main_authors": [ + "github.com/felixfeliz", + "github.com/eridur-de" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/paperfold/paperfold.inx b/extensions/fablabchemnitz/paperfold/paperfold.inx new file mode 100644 index 0000000..cf6e0bb --- /dev/null +++ b/extensions/fablabchemnitz/paperfold/paperfold.inx @@ -0,0 +1,108 @@ + + + Paperfold + fablabchemnitz.de.paperfold + + + + /your/beautiful/3dmodel/file + 200 + 1.0 + 3 + + + + + false + false + false + false + false + false + false + true + 0.0 + + + + + + + + false + ./inkscape_export/ + + + + + 15 + false + true + true + + + + + + false + 255 + 1943148287 + 3422552319 + 879076607 + + + + + + + false + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/paperfold/paperfold.py b/extensions/fablabchemnitz/paperfold/paperfold.py new file mode 100644 index 0000000..ef8057a --- /dev/null +++ b/extensions/fablabchemnitz/paperfold/paperfold.py @@ -0,0 +1,1021 @@ +#!/usr/bin/env python3 +import math +import inkex +from inkex import Transform, TextElement, Tspan, Color, Circle, PathElement, CubicSuperPath +import os +import random +import numpy as np +import openmesh as om +import networkx as nx +from lxml import etree +import copy + +""" +Extension for InkScape 1.0 + +Paperfold is another flattener for triangle mesh files, heavily based on paperfoldmodels by Felix Scholz aka felixfeliz. + +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 13.09.2020 +Last patch: 10.05.2021 +License: GNU GPL v3 + +To run this you need to install OpenMesh with python pip. + +The algorithm of paperfoldmodels consists of three steps: + - Find a minimum spanning tree of the dual graph of the mesh. + - Unfold the dual graph. + - Remove self-intersections by adding additional cuts along edges. + +Reference: The code is mostly based on the algorithm presented in a by Straub and Prautzsch (https://geom.ivd.kit.edu/downloads/proj-paper-models_cut_out_sheets.pdf). + +Module licenses +- paperfoldmodels (https://github.com/felixfeliz/paperfoldmodels) - MIT License + +possible import file types -> https://www.graphics.rwth-aachen.de/media/openmesh_static/Documentations/OpenMesh-8.0-Documentation/a04096.html + +todo: +- option to render all triangles in a detached way (overlapping lines/independent) + merge coplanar adjacent triangles to polygons +- write tab and slot generator (like joinery/polyhedra extension) +- fstl preview +- fix line: dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error (silent aborts) ... +- option to set fill color per face +- add some way to merge coplanar triangles (tri-faces) to polygons and keep those polygons (facets) intact. At the moment facets are getting destroyed. Not good for some papercrafts +""" + +class Paperfold(inkex.EffectExtension): + + angleRangeCalculated = False #set to true after first calculation iteration (needed globally) + minAngle = 0 + minAngle = 0 + angleRange = 0 + + def getElementChildren(self, element, elements = None): + if elements == None: + elements = [] + if element.tag != inkex.addNS('g','svg'): + elements.append(element) + for child in element.getchildren(): + self.getElementChildren(child, elements) + return elements + + # Compute the third point of a triangle when two points and all edge lengths are given + def getThirdPoint(self, v0, v1, l01, l12, l20): + v2rotx = (l01 ** 2 + l20 ** 2 - l12 ** 2) / (2 * l01) + val = (l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12) + v2roty0 = np.sqrt(abs(val)) / (2 * l01) + + v2roty1 = - v2roty0 + + theta = np.arctan2(v1[1] - v0[1], v1[0] - v0[0]) + + v2trans0 = np.array( + [v2rotx * np.cos(theta) - v2roty0 * np.sin(theta), v2rotx * np.sin(theta) + v2roty0 * np.cos(theta), 0]) + v2trans1 = np.array( + [v2rotx * np.cos(theta) - v2roty1 * np.sin(theta), v2rotx * np.sin(theta) + v2roty1 * np.cos(theta), 0]) + return [v2trans0 + v0, v2trans1 + v0] + + + # Check if two lines intersect + + + def lineIntersection(self, v1, v2, v3, v4, epsilon): + d = (v4[1] - v3[1]) * (v2[0] - v1[0]) - (v4[0] - v3[0]) * (v2[1] - v1[1]) + u = (v4[0] - v3[0]) * (v1[1] - v3[1]) - (v4[1] - v3[1]) * (v1[0] - v3[0]) + v = (v2[0] - v1[0]) * (v1[1] - v3[1]) - (v2[1] - v1[1]) * (v1[0] - v3[0]) + if d < 0: + u, v, d = -u, -v, -d + return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon)) + + # Check if a point lies inside a triangle + + + def pointInTriangle(self, A, B, C, P, epsilon): + v0 = [C[0] - A[0], C[1] - A[1]] + v1 = [B[0] - A[0], B[1] - A[1]] + v2 = [P[0] - A[0], P[1] - A[1]] + cross = lambda u, v: u[0] * v[1] - u[1] * v[0] + u = cross(v2, v0) + v = cross(v1, v2) + d = cross(v1, v0) + if d < 0: + u, v, d = -u, -v, -d + return u >= (0 + epsilon) and v >= (0 + epsilon) and (u + v) <= (d - epsilon) + + + # Check if two triangles intersect + + + def triangleIntersection(self, t1, t2, epsilon): + if self.lineIntersection(t1[0], t1[1], t2[0], t2[1], epsilon): return True + if self.lineIntersection(t1[0], t1[1], t2[0], t2[2], epsilon): return True + if self.lineIntersection(t1[0], t1[1], t2[1], t2[2], epsilon): return True + if self.lineIntersection(t1[0], t1[2], t2[0], t2[1], epsilon): return True + if self.lineIntersection(t1[0], t1[2], t2[0], t2[2], epsilon): return True + if self.lineIntersection(t1[0], t1[2], t2[1], t2[2], epsilon): return True + if self.lineIntersection(t1[1], t1[2], t2[0], t2[1], epsilon): return True + if self.lineIntersection(t1[1], t1[2], t2[0], t2[2], epsilon): return True + if self.lineIntersection(t1[1], t1[2], t2[1], t2[2], epsilon): return True + inTri = True + inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[0], epsilon) + inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[1], epsilon) + inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon) + if inTri == True: return True + inTri = True + inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[0], epsilon) + inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[1], epsilon) + inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon) + if inTri == True: return True + return False + + + # Functions for visualisation and output + + + def addVisualisationData(self, mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, dihedralAngles): + for i in range(3): + dihedralAngles[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), self.options.roundingDigits) + # Information, which edges belong together + glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx() + + # Function that unwinds a spanning tree + def unfoldSpanningTree(self, mesh, spanningTree): + try: + unfoldedMesh = om.TriMesh() # the unfolded mesh + + numFaces = mesh.n_faces() + sizeTree = spanningTree.number_of_edges() + numUnfoldedEdges = 3 * numFaces - sizeTree + + isFoldingEdge = np.zeros(numUnfoldedEdges, dtype=bool) # Indicates whether an edge is folded or cut + glueNumber = np.empty(numUnfoldedEdges, dtype=int) # Saves with which edge is glued together + dihedralAngles = np.empty(numUnfoldedEdges, dtype=float) # Valley folding or mountain folding + connections = np.empty(numFaces, dtype=int) # Saves which original triangle belongs to the unrolled one + + numFaces = mesh.n_faces() + sizeTree = spanningTree.number_of_edges() + numUnfoldedEdges = 3 * numFaces - sizeTree + + # Select the first triangle as desired + startingNode = list(spanningTree.nodes())[0] + startingTriangle = mesh.face_handle(startingNode) + + # We unwind the first triangle + + # All half edges of the first triangle + firstHalfEdge = mesh.halfedge_handle(startingTriangle) + secondHalfEdge = mesh.next_halfedge_handle(firstHalfEdge) + thirdHalfEdge = mesh.next_halfedge_handle(secondHalfEdge) + originalHalfEdges = [firstHalfEdge, secondHalfEdge, thirdHalfEdge] + + # Calculate the lengths of the edges, this will determine the shape of the triangle (congruence) + edgelengths = [mesh.calc_edge_length(firstHalfEdge), mesh.calc_edge_length(secondHalfEdge), + mesh.calc_edge_length(thirdHalfEdge)] + + # The first two points + firstUnfoldedPoint = np.array([0, 0, 0]) + secondUnfoldedPoint = np.array([edgelengths[0], 0, 0]) + + # We calculate the third point of the triangle from the first two. There are two possibilities + [thirdUnfolded0, thirdUnfolded1] = self.getThirdPoint(firstUnfoldedPoint, secondUnfoldedPoint, edgelengths[0], + edgelengths[1], + edgelengths[2]) + if thirdUnfolded0[1] > 0: + thirdUnfoldedPoint = thirdUnfolded0 + else: + thirdUnfoldePoint = thirdUnfolded1 + + # Add the new corners to the unwound net + firstUnfoldedVertex = unfoldedMesh.add_vertex(secondUnfoldedPoint) + secondUnfoldedVertex = unfoldedMesh.add_vertex(thirdUnfoldedPoint) + thirdUnfoldedVertex = unfoldedMesh.add_vertex(firstUnfoldedPoint) + + #firstUnfoldedVertex = unfoldedMesh.add_vertex(firstUnfoldedPoint) + #secondUnfoldedVertex = unfoldedMesh.add_vertex(secondUnfoldedPoint) + #thirdUnfoldedVertex = unfoldedMesh.add_vertex(thirdUnfoldedPoint) + + # Create the page + unfoldedFace = unfoldedMesh.add_face(firstUnfoldedVertex, secondUnfoldedVertex, thirdUnfoldedVertex) + + # Memory properties of the surface and edges + # The half edges in unrolled mesh + firstUnfoldedHalfEdge = unfoldedMesh.opposite_halfedge_handle(unfoldedMesh.halfedge_handle(firstUnfoldedVertex)) + secondUnfoldedHalfEdge = unfoldedMesh.next_halfedge_handle(firstUnfoldedHalfEdge) + thirdUnfoldedHalfEdge = unfoldedMesh.next_halfedge_handle(secondUnfoldedHalfEdge) + + unfoldedHalfEdges = [firstUnfoldedHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge] + + # Associated triangle in 3D mesh + connections[unfoldedFace.idx()] = startingTriangle.idx() + # Folding direction and adhesive number + self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, dihedralAngles) + + if self.angleRangeCalculated is False: + self.minAngle = min(dihedralAngles) + self.maxAngle = max(dihedralAngles) + #sometimes weird large value are returned, like -34345645435464565453356788761029782 + if self.minAngle < -180.0: + self.minAngle = -180.0 + if self.maxAngle > 180.0: + self.maxAngle = 180.0 + self.angleRange = self.maxAngle - self.minAngle + #self.msg(minAngle) + #self.msg(maxAngle) + #self.msg(angleRange) + self.angleRangeCalculated = True + + halfEdgeConnections = {firstHalfEdge.idx(): firstUnfoldedHalfEdge.idx(), + secondHalfEdge.idx(): secondUnfoldedHalfEdge.idx(), + thirdHalfEdge.idx(): thirdUnfoldedHalfEdge.idx()} + + # We walk through the tree + for dualEdge in nx.dfs_edges(spanningTree, source=startingNode): + try: + foldingEdge = mesh.edge_handle(spanningTree[dualEdge[0]][dualEdge[1]]['idx']) + # Find the corresponding half edge in the output triangle + foldingHalfEdge = mesh.halfedge_handle(foldingEdge, 0) + if not (mesh.face_handle(foldingHalfEdge).idx() == dualEdge[0]): + foldingHalfEdge = mesh.halfedge_handle(foldingEdge, 1) + + # Find the corresponding unwound half edge + unfoldedLastHalfEdge = unfoldedMesh.halfedge_handle(halfEdgeConnections[foldingHalfEdge.idx()]) + + # Find the point in the unrolled triangle that is not on the folding edge + oppositeUnfoldedVertex = unfoldedMesh.to_vertex_handle(unfoldedMesh.next_halfedge_handle(unfoldedLastHalfEdge)) + + # We turn the half edges over to lie in the new triangle + foldingHalfEdge = mesh.opposite_halfedge_handle(foldingHalfEdge) + unfoldedLastHalfEdge = unfoldedMesh.opposite_halfedge_handle(unfoldedLastHalfEdge) + + # The two corners of the folding edge + unfoldedFromVertex = unfoldedMesh.from_vertex_handle(unfoldedLastHalfEdge) + unfoldedToVertex = unfoldedMesh.to_vertex_handle(unfoldedLastHalfEdge) + + # Calculate the edge lengths in the new triangle + secondHalfEdgeInFace = mesh.next_halfedge_handle(foldingHalfEdge) + thirdHalfEdgeInFace = mesh.next_halfedge_handle(secondHalfEdgeInFace) + + originalHalfEdges = [foldingHalfEdge, secondHalfEdgeInFace, thirdHalfEdgeInFace] + + edgelengths = [mesh.calc_edge_length(foldingHalfEdge), mesh.calc_edge_length(secondHalfEdgeInFace), + mesh.calc_edge_length(thirdHalfEdgeInFace)] + + # We calculate the two possibilities for the third point in the triangle + [newUnfoldedVertex0, newUnfoldedVertex1] = self.getThirdPoint(unfoldedMesh.point(unfoldedFromVertex), + unfoldedMesh.point(unfoldedToVertex), edgelengths[0], + edgelengths[1], edgelengths[2]) + + + newUnfoldedVertex = unfoldedMesh.add_vertex(newUnfoldedVertex0) + + # Make the face + newface = unfoldedMesh.add_face(unfoldedFromVertex, unfoldedToVertex, newUnfoldedVertex) + + secondUnfoldedHalfEdge = unfoldedMesh.next_halfedge_handle(unfoldedLastHalfEdge) + thirdUnfoldedHalfEdge = unfoldedMesh.next_halfedge_handle(secondUnfoldedHalfEdge) + unfoldedHalfEdges = [unfoldedLastHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge] + + # Saving the information about edges and page + # Dotted one's in the output + unfoldedLastEdge = unfoldedMesh.edge_handle(unfoldedLastHalfEdge) + isFoldingEdge[unfoldedLastEdge.idx()] = True + + # Gluing number and folding direction + self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, dihedralAngles) + + # Related page + connections[newface.idx()] = dualEdge[1] + + # Identify the half edges + for i in range(3): + halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx() + except Exception as e: + inkex.utils.debug("Error walking the dual tree at dualEdge {}".format(e)) + exit(1) + return [unfoldedMesh, isFoldingEdge, connections, glueNumber, dihedralAngles] + except Exception as e: + inkex.utils.debug("Error: model could not be unfolded. Check for:") + inkex.utils.debug(" - watertight model / intact hull") + inkex.utils.debug(" - duplicated edges or faces") + inkex.utils.debug(" - detached faces or holes") + inkex.utils.debug(" - missing units") + inkex.utils.debug(" - missing coordinate system") + inkex.utils.debug(" - multiple bodies in one file") + exit(1) + + + def unfold(self, mesh): + # Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge + numEdges = mesh.n_edges() + numVertices = mesh.n_vertices() + numFaces = mesh.n_faces() + + if numFaces > self.options.maxNumFaces: + inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but only " + str( self.options.maxNumFaces) + " are allowed.") + exit(1) + + if self.options.printStats is True: + inkex.utils.debug("Input STL mesh stats:") + inkex.utils.debug("* Number of edges: " + str(numEdges)) + inkex.utils.debug("* Number of vertices: " + str(numVertices)) + inkex.utils.debug("* Number of faces: " + str(numFaces)) + inkex.utils.debug("-----------------------------------------------------------") + + # Generate the dual graph of the mesh and calculate the weights + dualGraph = nx.Graph() + + # For the weights: calculate the longest and shortest edge of the triangle + minLength = 1000 + maxLength = 0 + for edge in mesh.edges(): + edgelength = mesh.calc_edge_length(edge) + if edgelength < minLength: + minLength = edgelength + if edgelength > maxLength: + maxLength = edgelength + + # All edges in the net + for edge in mesh.edges(): + #inkex.utils.debug("edge.idx = " + str(edge.idx())) + + # The two sides adjacent to the edge + face1 = mesh.face_handle(mesh.halfedge_handle(edge, 0)) + face2 = mesh.face_handle(mesh.halfedge_handle(edge, 1)) + + # The weight + edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength) + + if self.options.experimentalWeights is True: + if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) > 0: + edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength) + if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) < 0: + edgeweight = -(1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength)) + if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) == 0: + edgeweight = 0.0 + + #inkex.utils.debug("edgeweight = " + str(edgeweight)) + # Calculate the centres of the pages (only necessary for visualisation) + center1 = (0, 0) + for vertex in mesh.fv(face1): + center1 = center1 + 0.3333333333333333 * np.array([mesh.point(vertex)[0], mesh.point(vertex)[2]]) + center2 = (0, 0) + for vertex in mesh.fv(face2): + center2 = center2 + 0.3333333333333333 * np.array([mesh.point(vertex)[0], mesh.point(vertex)[2]]) + + # Add the new nodes and edge to the dual graph + dualGraph.add_node(face1.idx(), pos=center1) + dualGraph.add_node(face2.idx(), pos=center2) + dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error ... + + # Calculate the minimum spanning tree + spanningTree = nx.minimum_spanning_tree(dualGraph) + + # Unfold the tree + fullUnfolding = self.unfoldSpanningTree(mesh, spanningTree) + [unfoldedMesh, isFoldingEdge, connections, glueNumber, dihedralAngles] = fullUnfolding + + + # Resolve the intersections + # Find all intersections + epsilon = 1E-12 # Accuracy + faceIntersections = [] + for face1 in unfoldedMesh.faces(): + for face2 in unfoldedMesh.faces(): + if face2.idx() < face1.idx(): # so that we do not double check the couples + # Get the triangle faces + triangle1 = [] + triangle2 = [] + for halfedge in unfoldedMesh.fh(face1): + triangle1.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge))) + for halfedge in unfoldedMesh.fh(face2): + triangle2.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge))) + if self.triangleIntersection(triangle1, triangle2, epsilon): + faceIntersections.append([connections[face1.idx()], connections[face2.idx()]]) + + # Find the paths + # We find the minimum number of cuts to resolve any self-intersection + + # Search all paths between overlapping triangles + paths = [] + for intersection in faceIntersections: + paths.append( + nx.algorithms.shortest_paths.shortest_path(spanningTree, source=intersection[0], target=intersection[1])) + + # Find all edges in all threads + edgepaths = [] + for path in paths: + edgepath = [] + for i in range(len(path) - 1): + edgepath.append((path[i], path[i + 1])) + edgepaths.append(edgepath) + + # List of all edges in all paths + allEdgesInPaths = list(set().union(*edgepaths)) + + # Count how often each edge occurs + numEdgesInPaths = [] + for edge in allEdgesInPaths: + num = 0 + for path in edgepaths: + if edge in path: + num = num + 1 + numEdgesInPaths.append(num) + + S = [] + C = [] + + while len(C) != len(paths): + # Calculate the weights to decide which edge to cut + cutWeights = np.empty(len(allEdgesInPaths)) + for i in range(len(allEdgesInPaths)): + currentEdge = allEdgesInPaths[i] + + # Count how many of the paths in which the edge occurs have already been cut + numInC = 0 + for path in C: + if currentEdge in path: + numInC = numInC + 1 + + # Determine the weight + if (numEdgesInPaths[i] - numInC) > 0: + cutWeights[i] = 1 / (numEdgesInPaths[i] - numInC) + else: + cutWeights[i] = 1000 # 1000 = infinite + # Find the edge with the least weight + minimalIndex = np.argmin(cutWeights) + S.append(allEdgesInPaths[minimalIndex]) + # Find all paths where the edge occurs and add them to C + for path in edgepaths: + if allEdgesInPaths[minimalIndex] in path and not path in C: + C.append(path) + + # Now we remove the cut edges from the minimum spanning tree + spanningTree.remove_edges_from(S) + + # Find the cohesive components + connectedComponents = nx.algorithms.components.connected_components(spanningTree) + connectedComponentList = list(connectedComponents) + + # Unfolding of the components + unfoldings = [] + for component in connectedComponentList: + unfoldings.append(self.unfoldSpanningTree(mesh, spanningTree.subgraph(component))) + + + return fullUnfolding, unfoldings + + + def findBoundingBox(self, mesh): + firstpoint = mesh.point(mesh.vertex_handle(0)) + xmin = firstpoint[0] + xmax = firstpoint[0] + ymin = firstpoint[1] + ymax = firstpoint[1] + for vertex in mesh.vertices(): + coordinates = mesh.point(vertex) + if (coordinates[0] < xmin): + xmin = coordinates[0] + if (coordinates[0] > xmax): + xmax = coordinates[0] + if (coordinates[1] < ymin): + ymin = coordinates[1] + if (coordinates[1] > ymax): + ymax = coordinates[1] + boxSize = np.maximum(np.abs(xmax - xmin), np.abs(ymax - ymin)) + + return [xmin, ymin, boxSize] + + + def writeSVG(self, unfolding, size, randomColorSet): + mesh = unfolding[0] + isFoldingEdge = unfolding[1] + glueNumber = unfolding[3] + dihedralAngles = unfolding[4] + + #statistic values + gluePairs = 0 + cuts = 0 + coplanarEdges = 0 + mountainFolds = 0 + valleyFolds = 0 + + # Calculate the bounding box + [xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0]) + + if size > 0: + boxSize = size + + strokewidth = boxSize * self.options.fontSize / 8000 + dashLength = boxSize * self.options.fontSize / 2000 + spaceLength = boxSize * self.options.fontSize / 800 + textDistance = boxSize * self.options.fontSize / 800 + textStrokeWidth = boxSize * self.options.fontSize / 3000 + fontsize = boxSize * self.options.fontSize / 1000 + + # Grouping + uniqueMainId = self.svg.get_unique_id("") + + paperfoldPageGroup = self.document.getroot().add(inkex.Group(id=uniqueMainId + "-paperfold-page")) + + textGroup = inkex.Group(id=uniqueMainId + "-text") + edgesGroup = inkex.Group(id=uniqueMainId + "-edges") + paperfoldPageGroup.add(textGroup) + paperfoldPageGroup.add(edgesGroup) + + textFacesGroup = inkex.Group(id=uniqueMainId + "-textFaces") + textEdgesGroup = inkex.Group(id=uniqueMainId + "-textEdges") + textGroup.add(textFacesGroup) + textGroup.add(textEdgesGroup) + + #we could write the unfolded mesh as a 2D stl file to disk if we like: + if self.options.writeTwoDSTL is True: + if not os.path.exists(self.options.TwoDSTLdir): + inkex.utils.debug("Export location for 2D STL unfoldings does not exist. Please select a another dir and try again.") + exit(1) + else: + om.write_mesh(os.path.join(self.options.TwoDSTLdir, uniqueMainId + "-paperfold-page.stl"), mesh) + + + ######################################################### + # Nmbering triangle faces with circle around + ######################################################### + if self.options.printTriangleNumbers is True: + for face in mesh.faces(): + centroid = mesh.calc_face_centroid(face) + textFaceGroup = inkex.Group(id=uniqueMainId + "-textFace-" + str(face.idx())) + + circle = textFaceGroup.add(Circle(cx="{:0.6f}".format(centroid[0]), cy="{:0.6f}".format(centroid[1]), r="{:0.6f}".format(fontsize))) + circle.set('id', uniqueMainId + "-textFaceCricle-" + str(face.idx())) + circle.set("style", "stroke:#000000;stroke-width:{:0.6f}".format(strokewidth/2) + ";fill:none") + + text = textFaceGroup.add(TextElement(id=uniqueMainId + "-textFaceNumber-" + str(face.idx()))) + text.set("x", "{:0.6f}".format(centroid[0])) + text.set("y", "{:0.6f}".format(centroid[1] + fontsize / 3)) + text.set("font-size", "{:0.6f}".format(fontsize)) + text.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center") + + tspan = text.add(Tspan(id=uniqueMainId + "-textFaceNumberTspan-" + str(face.idx()))) + tspan.set("x", "{:0.6f}".format(centroid[0])) + tspan.set("y", "{:0.6f}".format(centroid[1] + fontsize / 3)) + tspan.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center") + tspan.text = str(face.idx()) + textFacesGroup.append(textFaceGroup) + + ######################################################### + # Nmbering triangle edges and style them according to their type + ######################################################### + # Go over all edges of the grid + for edge in mesh.edges(): + # The two endpoints + he = mesh.halfedge_handle(edge, 0) + vertex0 = mesh.point(mesh.from_vertex_handle(he)) + vertex1 = mesh.point(mesh.to_vertex_handle(he)) + + # Write a straight line between the two corners + line = edgesGroup.add(PathElement()) + line.set('d', "M {:0.6f},{:0.6f} {:0.6f},{:0.6f}".format(vertex0[0], vertex0[1], vertex1[0], vertex1[1])) + # Colour depending on folding direction + lineStyle = {"fill": "none"} + + lineStyle.update({"stroke": self.options.colorCutEdges}) + line.set("id", uniqueMainId + "-cut-edge-" + str(edge.idx())) + + lineStyle.update({"stroke-width": "{:0.6f}".format(strokewidth)}) + lineStyle.update({"stroke-linecap":"butt"}) + lineStyle.update({"stroke-linejoin":"miter"}) + lineStyle.update({"stroke-miterlimit":"4"}) + + dihedralAngle = dihedralAngles[edge.idx()] + + # Dotted lines for folding edges + if isFoldingEdge[edge.idx()]: + if self.options.dashes is True: + lineStyle.update({"stroke-dasharray":"{:0.6f}, {:0.6f}".format(dashLength, spaceLength)}) + if dihedralAngle > 0: + lineStyle.update({"stroke": self.options.colorMountainFolds}) + line.set("id", uniqueMainId + "-mountain-fold-" + str(edge.idx())) + mountainFolds += 1 + if dihedralAngle < 0: + lineStyle.update({"stroke": self.options.colorValleyFolds}) + line.set("id", uniqueMainId + "-valley-fold-" + str(edge.idx())) + valleyFolds += 1 + if dihedralAngle == 0: + lineStyle.update({"stroke": self.options.colorCoplanarEdges}) + line.set("id", uniqueMainId + "-coplanar-edge-" + str(edge.idx())) + if self.options.importCoplanarEdges is False: + line.delete() + coplanarEdges += 1 + else: + lineStyle.update({"stroke-dasharray":"none"}) + + # The number of the edge to be glued + if not isFoldingEdge[edge.idx()]: + if self.options.separateGluePairsByColor is True: + lineStyle.update({"stroke": randomColorSet[glueNumber[edge.idx()]]}) + gluePairs += 1 + + lineStyle.update({"stroke-dashoffset":"0.0"}) + lineStyle.update({"stroke-opacity":"1.0"}) + + if self.options.edgeStyle == "saturationsForAngles": + if dihedralAngle != 0: #we dont want to apply HSL adjustments for zero angle lines because they would be invisible then + hslColor = inkex.Color(lineStyle.get('stroke')).to_hsl() + newSaturation = abs(dihedralAngle / self.angleRange) * 100 #percentage values + hslColor.saturation = newSaturation + lineStyle.update({"stroke":hslColor.to_rgb()}) + + elif self.options.edgeStyle == "opacitiesForAngles": + if dihedralAngle != 0: #we dont want to apply opacity adjustments for zero angle lines because they would be invisible then + opacity = abs(dihedralAngle / 180) + lineStyle.update({"stroke-opacity": "{:0.6f}".format(opacity)}) + + line.style = lineStyle + + ######################################################### + # Textual things + ######################################################### + halfEdge = mesh.halfedge_handle(edge, 0) # Find halfedge in the face + if mesh.face_handle(halfEdge).idx() == -1: + halfEdge = mesh.opposite_halfedge_handle(halfEdge) + vector = mesh.calc_edge_vector(halfEdge) + vector = vector / np.linalg.norm(vector) # normalize + midPoint = 0.5 * ( + mesh.point(mesh.from_vertex_handle(halfEdge)) + mesh.point(mesh.to_vertex_handle(halfEdge))) + rotatedVector = np.array([-vector[1], vector[0], 0]) + angle = np.arctan2(vector[1], vector[0]) + position = midPoint + textDistance * rotatedVector + if self.options.flipLabels is True: + position = midPoint - textDistance * rotatedVector + rotation = 180 / np.pi * angle + if self.options.flipLabels is True: + rotation += 180 + + text = textEdgesGroup.add(TextElement(id=uniqueMainId + "-edgeNumber-" + str(edge.idx()))) + text.set("x", "{:0.6f}".format(position[0])) + text.set("y", "{:0.6f}".format(position[1])) + text.set("font-size", "{:0.6f}".format(fontsize)) + text.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center") + text.set("transform", "rotate({:0.6f} {:0.6f} {:0.6f})".format(rotation, position[0], position[1])) + + tspan = text.add(Tspan()) + tspan.set("x", "{:0.6f}".format(position[0])) + tspan.set("y", "{:0.6f}".format(position[1])) + tspan.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center") + tspanText = [] + if self.options.printGluePairNumbers is True and not isFoldingEdge[edge.idx()]: + tspanText.append(str(glueNumber[edge.idx()])) + if self.options.printAngles is True and dihedralAngle != 0.0: + tspanText.append("{:0.2f}°".format(dihedralAngle)) + if self.options.printLengths is True: + printUnit = True + if printUnit is False: + unitToPrint = self.svg.unit + else: + unitToPrint = "" + tspanText.append("{:0.2f} {}".format(self.options.scalefactor * math.hypot(vertex1[0] - vertex0[0], vertex1[1] - vertex0[1]), unitToPrint)) + tspan.text = " | ".join(tspanText) + + if tspan.text == "": #if no text we remove again to clean up + text.delete() + tspan.delete() + + ''' + merge cutting edges to single contour. code ripped off from "join path" extension + ''' + if self.options.merge_cut_lines is True: + cutEdges = [] + + #find all cutting edges - they have to be sorted to build up a clean continuous line + for edge in edgesGroup: + edge_id = edge.get('id') + if "cut-edge-" in edge_id: + cutEdges.append(edge) + + #find the cutting edge which starts at the previous cutting edge end point + paths = {p.get('id'): self.getPartsFromCubicSuper(CubicSuperPath(p.get('d'))) for p in cutEdges } + pathIds = [p.get('id') for p in cutEdges] + + startPathId = pathIds[0] + pathIds = self.getArrangedIds(paths, startPathId) + + newParts = [] + firstElem = None + for key in pathIds: + parts = paths[key] + # ~ parts = getPartsFromCubicSuper(cspath) + start = parts[0][0][0] + elem = self.svg.getElementById(key) + + if(len(newParts) == 0): + newParts += parts[:] + firstElem = elem + else: + if(self.vectCmpWithMargin(start, newParts[-1][-1][-1], margin = .01)): + newParts[-1] += parts[0] + else: + newSeg = [newParts[-1][-1][-1], newParts[-1][-1][-1], start, start] + newParts[-1].append(newSeg) + newParts[-1] += parts[0] + + if(len(parts) > 1): + newParts += parts[1:] + + parent = elem.getparent() + parent.remove(elem) + + newElem = copy.copy(firstElem) + oldId = firstElem.get('id') + newElem.set('d', CubicSuperPath(self.getCubicSuperFromParts(newParts))) + newElem.set('id', oldId + '_joined') + parent.append(newElem) #insert at the end + + if len(textFacesGroup) == 0: + textFacesGroup.delete() #delete if empty set + + if len(textEdgesGroup) == 0: + textEdgesGroup.delete() #delete if empty set + + if len(textGroup) == 0: + textGroup.delete() #delete if empty set + + if self.options.printStats is True: + inkex.utils.debug(" * Number of cuts: " + str(cuts)) + inkex.utils.debug(" * Number of coplanar edges: " + str(coplanarEdges)) + inkex.utils.debug(" * Number of mountain folds: " + str(mountainFolds)) + inkex.utils.debug(" * Number of valley folds: " + str(valleyFolds)) + inkex.utils.debug(" * Number of glue pairs: {:0.0f}".format(gluePairs / 2)) + inkex.utils.debug(" * min angle: {:0.2f}".format(self.minAngle)) + inkex.utils.debug(" * max angle: {:0.2f}".format(self.maxAngle)) + inkex.utils.debug(" * Edge angle range: {:0.2f}".format(self.angleRange)) + + return paperfoldPageGroup + + + def floatCmpWithMargin(self, float1, float2, margin): + return abs(float1 - float2) < margin + + + def vectCmpWithMargin(self, vect1, vect2, margin): + return all(self.floatCmpWithMargin(vect2[i], vect1[i], margin) for i in range(0, len(vect1))) + + + def getPartsFromCubicSuper(self, cspath): + parts = [] + for subpath in cspath: + part = [] + prevBezPt = None + for i, bezierPt in enumerate(subpath): + if(prevBezPt != None): + seg = [prevBezPt[1], prevBezPt[2], bezierPt[0], bezierPt[1]] + part.append(seg) + prevBezPt = bezierPt + parts.append(part) + return parts + + + def getCubicSuperFromParts(self, parts): + cbsuper = [] + for part in parts: + subpath = [] + lastPt = None + pt = None + for seg in part: + if(pt == None): + ptLeft = seg[0] + pt = seg[0] + ptRight = seg[1] + subpath.append([ptLeft, pt, ptRight]) + ptLeft = seg[2] + pt = seg[3] + subpath.append([ptLeft, pt, pt]) + cbsuper.append(subpath) + return cbsuper + + + def getArrangedIds(self, pathMap, startPathId): + nextPathId = startPathId + orderPathIds = [nextPathId] + + #Arrange in order + while(len(orderPathIds) < len(pathMap)): + minDist = 9e+100 #A large float + closestId = None + np = pathMap[nextPathId] + npPts = [np[-1][-1][-1]] + if(len(orderPathIds) == 1):#compare both the ends for the first path + npPts.append(np[0][0][0]) + + for key in pathMap: + if(key in orderPathIds): + continue + parts = pathMap[key] + start = parts[0][0][0] + end = parts[-1][-1][-1] + + for i, npPt in enumerate(npPts): + dist = abs(start[0] - npPt[0]) + abs(start[1] - npPt[1]) + if(dist < minDist): + minDist = dist + closestId = key + dist = abs(end[0] - npPt[0]) + abs(end[1] - npPt[1]) + if(dist < minDist): + minDist = dist + pathMap[key] = [[[pts for pts in reversed(seg)] for seg in \ + reversed(part)] for part in reversed(parts)] + closestId = key + + #If start point of the first path is closer reverse its direction + if(i > 0 and closestId == key): + pathMap[nextPathId] = [[[pts for pts in reversed(seg)] for seg in \ + reversed(part)] for part in reversed(np)] + + orderPathIds.append(closestId) + nextPathId = closestId + return orderPathIds + + + def add_arguments(self, pars): + pars.add_argument("--tab") + + #Input + pars.add_argument("--inputfile") + pars.add_argument("--maxNumFaces", type=int, default=200, help="If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it.") + pars.add_argument("--scalefactor", type=float, default=1.0, help="Manual scale factor") + pars.add_argument("--roundingDigits", type=int, default=3, help="Digits for rounding") + + #Output + pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges") + pars.add_argument("--printAngles", type=inkex.Boolean, default=False, help="Print folding angles on edges") + pars.add_argument("--printLengths", type=inkex.Boolean, default=False, help="Print lengths on edges") + pars.add_argument("--printTriangleNumbers", type=inkex.Boolean, default=False, help="Print triangle numbers on faces") + pars.add_argument("--importCoplanarEdges", type=inkex.Boolean, default=False, help="Import coplanar edges") + pars.add_argument("--experimentalWeights", type=inkex.Boolean, default=False, help="Mess around with algorithm") + pars.add_argument("--printStats", type=inkex.Boolean, default=False, help="Show some unfold statistics") + pars.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box") + pars.add_argument("--extraborder", type=float, default=0.0) + pars.add_argument("--extraborderUnits") + pars.add_argument("--writeTwoDSTL", type=inkex.Boolean, default=False, help="Write 2D STL unfoldings") + pars.add_argument("--TwoDSTLdir", default="./inkscape_export/", help="Location to save exported 2D STL") + + #Style + pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)") + pars.add_argument("--flipLabels", type=inkex.Boolean, default=False, help="Flip labels") + pars.add_argument("--dashes", type=inkex.Boolean, default=True, help="Dashes for cut/coplanar edges") + pars.add_argument("--merge_cut_lines", type=inkex.Boolean, default=True, help="Merge cut lines") + pars.add_argument("--edgeStyle", help="Adjust color saturation or opacity for folding edges. The larger the angle the darker the color") + pars.add_argument("--separateGluePairsByColor", type=inkex.Boolean, default=False, help="Separate glue pairs by color") + pars.add_argument("--colorCutEdges", type=Color, default='255', help="Cut edges") + pars.add_argument("--colorCoplanarEdges", type=Color, default='1943148287', help="Coplanar edges") + pars.add_argument("--colorValleyFolds", type=Color, default='3422552319', help="Valley fold edges") + pars.add_argument("--colorMountainFolds", type=Color, default='879076607', help="Mountain fold edges") + + #Post Processing + pars.add_argument("--joineryMode", type=inkex.Boolean, default=False, help="Enable joinery mode") + pars.add_argument("--origamiSimulatorMode", type=inkex.Boolean, default=False, help="Enable origami simulator mode") + + + def effect(self): + if not os.path.exists(self.options.inputfile): + inkex.utils.debug("The input file does not exist. Please select a proper file and try again.") + exit(1) + mesh = om.read_trimesh(self.options.inputfile) + #mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that ONLY + + fullUnfolded, unfoldedComponents = self.unfold(mesh) + unfoldComponentCount = len(unfoldedComponents) + + #if len(unfoldedComponents) == 0: + # inkex.utils.debug("Error: no components were unfolded.") + # exit(1) + + if self.options.printStats is True: + inkex.utils.debug("Unfolding components: {:0.0f}".format(unfoldComponentCount)) + + # Compute maxSize of the components + # All components must be scaled to the same size as the largest component + maxSize = 0 + for unfolding in unfoldedComponents: + [xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0]) + if boxSize > maxSize: + maxSize = boxSize + + xSpacing = maxSize / unfoldComponentCount * 0.1 # 10% spacing between each component; calculated by max box size + + ######################################################### + # mode config for joinery: + ######################################################### + if self.options.joineryMode is True: + self.options.separateGluePairsByColor = True #we need random colors in this mode + + + ######################################################### + # mode config for origami simulator: + ######################################################### + ''' + required style for Origami Simulator: + colors: + - #ff0000 (red) - mountain folds + - #0000ff (blue) - valley folds + - #000000 (black) - boundary cuts (for both the outline of the pattern and any internal holes) + - #ffff00 (yellow) - coplonar triangle edges ("facet creases") (no support for polygons > 3 edges) + - #00ff00 (green) - thin slits + - #ff00ff (magenta) - undriven creases (swing freely) + + opacity: + - final fold angle of a mountain or valley fold is set by its opacity. Any fold angle between 0° and 180° may be used. For example: + - 1.0 = 180° (fully folded) + - 0.5 = 90° + - 0 = 0° (flat) + ''' + if self.options.origamiSimulatorMode is True: + self.options.joineryMode = True #we set to true even if false because we need the same flat structure for origami simulator + self.options.separateGluePairsByColor = False #we need to have no weird random colors in this mode + self.options.edgeStyle = "opacitiesForAngles" #highly important for simulation + self.options.dashes = False + self.options.printGluePairNumbers = False + self.options.printAngles = False + self.options.printLengths = False + self.options.importCoplanarEdges = True + self.options.colorCutEdges = "#000000" #black + self.options.colorCoplanarEdges = "#ffff00" #yellow + self.options.colorMountainFolds = "#ff0000" #red + self.options.colorValleyFolds = "#0000ff" #blue + + #generate random colors; used to identify glue tab pairs + randomColorSet = [] + if self.options.separateGluePairsByColor: + while len(randomColorSet) < len(mesh.edges()): + r = lambda: random.randint(0,255) + newColor = '#%02X%02X%02X' % (r(),r(),r()) + if newColor not in randomColorSet: + randomColorSet.append(newColor) + + # Create a new container group to attach all paperfolds + paperfoldMainGroup = self.document.getroot().add(inkex.Group(id=self.svg.get_unique_id("paperfold-"))) #make a new group at root level + for i in range(len(unfoldedComponents)): + if self.options.printStats is True: + inkex.utils.debug("-----------------------------------------------------------") + inkex.utils.debug("Unfolding component nr.: {:0.0f}".format(i)) + paperfoldPageGroup = self.writeSVG(unfoldedComponents[i], maxSize, randomColorSet) + #translate the groups next to each other to remove overlappings + if i != 0: + #previous_bbox = paperfoldMainGroup[i-1].bounding_box() + #as TextElement, Tspan and Circle cause wrong BBox calculation, we have to make it more complex + previous_bbox = inkex.BoundingBox() + for child in self.getElementChildren(paperfoldMainGroup[i-1]): + if not isinstance (child, inkex.TextElement) and \ + not isinstance (child, inkex.Tspan) and \ + not isinstance (child, inkex.Circle): + transform = inkex.Transform() + parent = child.getparent() + if parent is not None and isinstance(parent, inkex.ShapeElement): + transform = parent.composed_transform() + previous_bbox += child.bounding_box(transform) + + #this_bbox = paperfoldPageGroup.bounding_box() + this_bbox = inkex.BoundingBox() + for child in self.getElementChildren(paperfoldPageGroup): + #as TextElement, Tspan and Circle cause wrong BBox calculation, we have to make it more complex + if not isinstance (child, inkex.TextElement) and \ + not isinstance (child, inkex.Tspan) and \ + not isinstance (child, inkex.Circle): + transform = inkex.Transform() + parent = child.getparent() + if parent is not None and isinstance(parent, inkex.ShapeElement): + transform = parent.composed_transform() + this_bbox += child.bounding_box(transform) + + #self.msg(previous_bbox) + #self.msg(this_bbox) + paperfoldPageGroup.set("transform", "translate({:0.6f}, 0.0)".format(previous_bbox.left + previous_bbox.width - this_bbox.left + xSpacing)) + paperfoldMainGroup.append(paperfoldPageGroup) + + #apply scale factor + translation_matrix = [[self.options.scalefactor, 0.0, 0.0], [0.0, self.options.scalefactor, 0.0]] + paperfoldMainGroup.transform = Transform(translation_matrix) @ paperfoldMainGroup.transform + #paperfoldMainGroup.set('transform', 'scale(%f,%f)' % (self.options.scalefactor, self.options.scalefactor)) + + #adjust canvas to the inserted unfolding + if self.options.resizetoimport: + bbox = paperfoldMainGroup.bounding_box() + namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')) + root = self.svg.getElement('//svg:svg'); + offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborderUnits) + root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset)) + root.set('width', "{:0.6f}{}".format(bbox.width + 2 * offset, self.svg.unit)) + root.set('height', "{:0.6f}{}".format(bbox.height + 2 * offset, self.svg.unit)) + + #if set, we move all edges (path elements) to the top level + if self.options.joineryMode is True: + for paperfoldPage in paperfoldMainGroup.getchildren(): + for child in paperfoldPage: + if "-edges" in child.get('id'): + for edge in child: + edgeTransform = edge.composed_transform() + self.document.getroot().append(edge) + edge.transform = edgeTransform + + +if __name__ == '__main__': + Paperfold().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/styles_to_layers/meta.json b/extensions/fablabchemnitz/styles_to_layers/meta.json new file mode 100644 index 0000000..eaffec2 --- /dev/null +++ b/extensions/fablabchemnitz/styles_to_layers/meta.json @@ -0,0 +1,23 @@ +[ + { + "name": "Styles To Layers", + "id": "fablabchemnitz.de.styles_to_layers", + "path": "styles_to_layers", + "dependent_extensions": [ + "apply_transformations", + "remove_empty_groups" + ], + "original_name": "Styles To Layers", + "original_id": "fablabchemnitz.de.styles_to_layers", + "license": "GNU GPL v3", + "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE", + "comment": "written by Mario Voigt", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/styles_to_layers", + "fork_url": null, + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Styles+To+Layers", + "inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85styles-to-layers", + "main_authors": [ + "github.com/eridur-de" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.inx b/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.inx new file mode 100644 index 0000000..7faef96 --- /dev/null +++ b/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.inx @@ -0,0 +1,67 @@ + + + Styles To Layers + fablabchemnitz.de.styles_to_layers + + + + + + + + + + + + + + + + + + 1 + 1 + false + true + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.py b/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.py new file mode 100644 index 0000000..f24d485 --- /dev/null +++ b/extensions/fablabchemnitz/styles_to_layers/styles_to_layers.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 + +""" +Extension for InkScape 1.0 +Features + - filters the current selection or the whole document for fill or stroke style. Each style will be put onto it's own layer. This way you can devide elements by their colors. + +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 19.08.2020 +Last patch: 17.10.2021 +License: GNU GPL v3 +""" +import inkex +import re +import sys +from lxml import etree +import math +from operator import itemgetter +from inkex.colors import Color + +sys.path.append("../remove_empty_groups") +sys.path.append("../apply_transformations") + +class StylesToLayers(inkex.EffectExtension): + + def findLayer(self, layerName): + svg_layers = self.document.xpath('//svg:g[@inkscape:groupmode="layer"]', namespaces=inkex.NSS) + for layer in svg_layers: + #self.msg(str(layer.get('inkscape:label')) + " == " + layerName) + if layer.get('inkscape:label') == layerName: + return layer + return None + + def createLayer(self, layerNodeList, layerName): + svg = self.document.xpath('//svg:svg',namespaces=inkex.NSS)[0] + for layer in layerNodeList: + #self.msg(str(layer[0].get('inkscape:label')) + " == " + layerName) + if layer[0].get('inkscape:label') == layerName: + return layer[0] #already exists. Do not create duplicate + layer = etree.SubElement(svg, 'g') + layer.set(inkex.addNS('label', 'inkscape'), '%s' % layerName) + layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') + return layer + + def add_arguments(self, pars): + pars.add_argument("--tab") + pars.add_argument("--apply_transformations", type=inkex.Boolean, default=False, help="Run 'Apply Transformations' extension before running vpype. Helps avoiding geometry shifting") + pars.add_argument("--separateby", default = "stroke", help = "Separate by") + pars.add_argument("--sortcolorby", default = "hexval", help = "Sort colors by") + pars.add_argument("--subdividethreshold", type=int, default = 1, help = "Threshold for splitting into sub layers") + pars.add_argument("--decimals", type=int, default = 1, help = "Decimal tolerance") + pars.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Cleanup all unused groups/layers (requires separate extension)") + pars.add_argument("--put_unfiltered", type=inkex.Boolean, default = False, help = "Put unfiltered elements to a separate layer") + pars.add_argument("--show_info", type=inkex.Boolean, default = False, help = "Show elements which have no style attributes to filter") + + def effect(self): + + def colorsort(stroke_value): #this function applies to stroke or fill (hex colors) + if self.options.sortcolorby == "hexval": + return float(int(stroke_value[1:], 16)) + elif self.options.sortcolorby == "hue": + return float(Color(stroke_value).to_hsl()[0]) + elif self.options.sortcolorby == "saturation": + return float(Color(stroke_value).to_hsl()[1]) + elif self.options.sortcolorby == "luminance": + return float(Color(stroke_value).to_hsl()[2]) + return None + + applyTransformationsAvailable = False # at first we apply external extension + try: + import apply_transformations + applyTransformationsAvailable = True + except Exception as e: + # self.msg(e) + self.msg("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") + + layer_name = None + layerNodeList = [] #list with layer, neutral_value, element and self.options.separateby type + selected = [] #list of items to parse + + if len(self.svg.selected) == 0: + for element in self.document.getroot().iter("*"): + selected.append(element) + else: + selected = self.svg.selected.values() + + for element in selected: + + # additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc. + if self.options.apply_transformations is True and applyTransformationsAvailable is True: + apply_transformations.ApplyTransformations().recursiveFuseTransform(element) + + if isinstance(element, inkex.ShapeElement): # Elements which have a visible representation on the canvas (even without a style attribute but by their type); if we do not use that ifInstance Filter we provokate unkown InkScape fatal crashes + + style = element.style + if style is not None: + #if no style attributes or stroke/fill are set as extra attribute + stroke = element.get('stroke') + stroke_width = element.get('stroke-width') + stroke_opacity = element.get('stroke-opacity') + fill = element.get('fill') + fill_opacity = element.get('fill-opacity') + + # possible values for fill are #HEXCOLOR (like #000000), color name (like purple, black, red) or gradients (URLs) + + neutral_value = None #we will use this value to slice the filter result into sub layers (threshold) + + if fill is not None: + style['fill'] = fill + if stroke is not None: + style['stroke'] = stroke + + #we don't want to destroy elements with gradients (they contain svg:stop elements which have a style too) and we don't want to mess with tspans (text) + #the Styles to Layers extension still might brick the gradients (some tests failed) + if style and element.tag != inkex.addNS('stop','svg') and element.tag != inkex.addNS('tspan','svg'): + + if self.options.separateby == "element_tag": + neutral_value = 1 + layer_name = "element_tag-" + element.tag.replace("{http://www.w3.org/2000/svg}", "") + + elif self.options.separateby == "stroke": + stroke = style.get('stroke') + if stroke is not None and stroke != "none": + stroke_converted = str(Color(stroke).to_rgb()) #the color can be hex code or clear name. we handle both the same + neutral_value = colorsort(stroke_converted) + layer_name = "stroke-{}-{}".format(self.options.sortcolorby, stroke_converted) + else: + layer_name = "stroke-{}-none".format(self.options.sortcolorby) + + elif self.options.separateby == "stroke_width": + stroke_width = style.get('stroke-width') + if stroke_width is not None: + neutral_value = self.svg.unittouu(stroke_width) + layer_name = "stroke-width-{}".format(neutral_value) + else: + layer_name = "stroke-width-none" + + elif self.options.separateby == "stroke_hairline": + inkscape_stroke = style.get('-inkscape-stroke') + if inkscape_stroke is not None and inkscape_stroke == "hairline": + neutral_value = 1 + layer_name = "stroke-hairline-yes" + else: + neutral_value = 0 + layer_name = "stroke-hairline-no" + + elif self.options.separateby == "stroke_opacity": + stroke_opacity = style.get('stroke-opacity') + if stroke_opacity is not None: + neutral_value = float(stroke_opacity) + layer_name = "stroke-opacity-{}".format(neutral_value) + else: + layer_name = "stroke-opacity-none" + + elif self.options.separateby == "fill": + fill = style.get('fill') + if fill is not None: + #check if the fill color is a real color or a gradient. if it's a gradient we skip the element + if fill != "none" and "url" not in fill: + fill_converted = str(Color(fill).to_rgb()) #the color can be hex code or clear name. we handle both the same + neutral_value = colorsort(fill_converted) + layer_name = "fill-{}-{}".format(self.options.sortcolorby,fill_converted) + elif "url" in fill: #okay we found a gradient. we put it to some group + layer_name = "fill-{}-gradient".format(self.options.sortcolorby) + else: #none + layer_name = "fill-" + self.options.sortcolorby + "-none" + else: + layer_name = "fill-" + self.options.sortcolorby + "-none" + + elif self.options.separateby == "fill_opacity": + fill_opacity = style.get('fill-opacity') + if fill_opacity is not None: + neutral_value = float(fill_opacity) + layer_name = "fill-opacity-{}".format(neutral_value) + else: + layer_name = "fill-opacity-none" + + else: + self.msg("No proper option selected.") + exit(1) + + if neutral_value is not None: #apply decimals filter + neutral_value = float(round(neutral_value, self.options.decimals)) + if layer_name is not None: + layer_name = layer_name.split(";")[0] #cut off existing semicolons to avoid duplicated layers with/without semicolon + currentLayer = self.findLayer(layer_name) + if currentLayer is None: #layer does not exist, so create a new one + layerNodeList.append([self.createLayer(layerNodeList, layer_name), neutral_value, element, self.options.separateby]) + else: + layerNodeList.append([currentLayer, neutral_value, element, self.options.separateby]) #layer is existent. append items to this later + elif layer_name is None and self.options.put_unfiltered: + layer_name = 'without-' + self.options.separateby + '-in-style-attribute' + else: #if no style attribute in element and not a group + if isinstance(element, inkex.Group) is False: + if self.options.show_info: + self.msg(element.get('id') + ' has no style attribute') + if self.options.put_unfiltered: + layer_name = 'without-style-attribute' + currentLayer = self.findLayer(layer_name) + + if currentLayer is None: #layer does not exist, so create a new one + layerNodeList.append([self.createLayer(layerNodeList, layer_name), None, element, None]) + else: + layerNodeList.append([currentLayer, None, element, None]) #layer is existent. append items to this later + + contentlength = 0 #some counter to track if there are layers inside or if it is just a list with empty children + for layerNode in layerNodeList: + try: #some nasty workaround. Make better code + layerNode[0].append(layerNode[2]) #append element to created layer + if layerNode[1] is not None: contentlength += 1 #for each found layer we increment +1 + except: + continue + + # we do some cosmetics with layers. Sometimes it can happen that one layer includes another. We don't want that. We move all layers to the top level + for newLayerNode in layerNodeList: + self.document.getroot().append(newLayerNode[0]) + + # Additionally if threshold was defined re-arrange the previously created layers by putting them into sub layers + if self.options.subdividethreshold > 1 and contentlength > 0: #check if we need to subdivide and if there are items we could rearrange into sub layers + + #disabled sorting because it can return NoneType values which will kill the algorithm + #layerNodeList.sort(key=itemgetter(1)) #sort by neutral values from min to max to put them with ease into parent layers + + topLevelLayerNodeList = [] #list with new layers and sub layers (mapping) + minmax_range = [] + for layerNode in layerNodeList: + if layerNode[1] is not None: + if layerNode[1] not in minmax_range: + minmax_range.append(layerNode[1]) #get neutral_value + + if len(minmax_range) >= 3: #if there are less than 3 distinct values a sub-layering will make no sense + #adjust the subdividethreshold if there are less layers than division threshold value dictates + if len(minmax_range) - 1 < self.options.subdividethreshold: + self.options.subdividethreshold = len(minmax_range)-1 + #calculate the sub layer slices (sub ranges) + minval = min(minmax_range) + maxval = max(minmax_range) + sliceinterval = (maxval - minval) / self.options.subdividethreshold + + #self.msg("neutral values (sorted) = " + str(minmax_range)) + #self.msg("min neutral_value = " + str(minval)) + #self.msg("max neutral_value = " + str(maxval)) + #self.msg("slice value (divide step size) = " + str(sliceinterval)) + #self.msg("subdivides (parent layers) = " + str(self.options.subdividethreshold)) + + for layerNode in layerNodeList: + for x in range(0, self.options.subdividethreshold): #loop through the sorted neutral_values and determine to which layer they should belong + + if layerNode[1] is None: + layer_name = str(layerNode[3]) + "#parent:unfilterable" + currentLayer = self.findLayer(layer_name) + if currentLayer is None: #layer does not exist, so create a new one + topLevelLayerNodeList.append([self.createLayer(topLevelLayerNodeList, layer_name), layerNode[0]]) + else: + topLevelLayerNodeList.append([currentLayer, layerNode[0]]) #layer is existent. append items to this later + break + else: + layer_name = str(layerNode[3]) + "#parent" + str(x+1) + currentLayer = self.findLayer(layer_name) + #value example for arranging: + #min neutral_value = 0.07 + #max neutral_value = 2.50 + #slice value = 0.81 + #subdivides = 3 + # + #that finally should generate: + # layer #1: (from 0.07) to (0.07 + 0.81 = 0.88) + # layer #2: (from 0.88) to (0.88 + 0.81 = 1.69) + # layer #3: (from 1.69) to (1.69 + 0.81 = 2.50) + # + #now check layerNode[1] (neutral_value) and sort it into the correct layer + if (layerNode[1] >= minval + sliceinterval * x) and (layerNode[1] <= minval + sliceinterval + sliceinterval * x): + if currentLayer is None: #layer does not exist, so create a new one + topLevelLayerNodeList.append([self.createLayer(topLevelLayerNodeList, layer_name), layerNode[0]]) + else: + topLevelLayerNodeList.append([currentLayer, layerNode[0]]) #layer is existent. append items to this later + break + + #finally append the sublayers to the slices + #for layer in topLevelLayerNodeList: + #self.msg(layer[0].get('inkscape:label')) + #self.msg(layer[1]) + for newLayerNode in topLevelLayerNodeList: + newLayerNode[0].append(newLayerNode[1]) #append newlayer to layer + + #clean all empty layers from node list. Please note that the following remove_empty_groups + #call does not apply for this so we need to do it as PREVIOUS step before! + for i in range(0, len(layerNodeList)): + deletes = [] + for j in range(0, len(layerNodeList[i][0])): + if len(layerNodeList[i][0][j]) == 0 and isinstance(layerNodeList[i][0][j], inkex.Group): + deletes.append(layerNodeList[i][0][j]) + for delete in deletes: + delete.getparent().remove(delete) + if len(layerNodeList[i][0]) == 0: + if layerNodeList[i][0].getparent() is not None: + layerNodeList[i][0].getparent().remove(layerNodeList[i][0]) + + if self.options.cleanup == True: + try: + import remove_empty_groups + remove_empty_groups.RemoveEmptyGroups.effect(self) + except: + self.msg("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") + +if __name__ == '__main__': + StylesToLayers().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/sudoku/meta.json b/extensions/fablabchemnitz/sudoku/meta.json new file mode 100644 index 0000000..acb789e --- /dev/null +++ b/extensions/fablabchemnitz/sudoku/meta.json @@ -0,0 +1,21 @@ +[ + { + "name": "Sundial Declining", + "id": "fablabchemnitz.de.sundial_declining", + "path": "sundial_declining", + "dependent_extensions": null, + "original_name": "Sundial", + "original_id": "fr.electropol.tableausimple.inkscape", + "license": "Public Domain", + "license_url": "https://inkscape.org/de/~TomasUrban/%E2%98%85sundial-declining", + "comment": "ported to Inkscape v1 by Mario Voigt", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/sundial_declining", + "fork_url": "https://inkscape.org/de/~TomasUrban/%E2%98%85sundial-declining", + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Sundial+Declining", + "inkscape_gallery_url": null, + "main_authors": [ + "inkscape.org/TomasUrban", + "github.com/eridur-de" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/sudoku/qqwing b/extensions/fablabchemnitz/sudoku/qqwing new file mode 100755 index 0000000..2ab35c6 --- /dev/null +++ b/extensions/fablabchemnitz/sudoku/qqwing @@ -0,0 +1,210 @@ +#! /bin/sh + +# qqwing - temporary wrapper script for .libs/qqwing +# Generated by libtool (GNU libtool) 2.4.2 Debian-2.4.2-1.10ubuntu1 +# +# The qqwing program cannot be directly executed until all the libtool +# libraries that it depends on are installed. +# +# This wrapper script should never be moved out of the build directory. +# If it is, it will not operate correctly. + +# Sed substitution that helps us do robust quoting. It backslashifies +# metacharacters that are still active within double-quoted strings. +sed_quote_subst='s/\([`"$\\]\)/\\\1/g' + +# Be Bourne compatible +if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then + emulate sh + NULLCMD=: + # Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else + case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac +fi +BIN_SH=xpg4; export BIN_SH # for Tru64 +DUALCASE=1; export DUALCASE # for MKS sh + +# The HP-UX ksh and POSIX shell print the target directory to stdout +# if CDPATH is set. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +relink_command="" + +# This environment variable determines our operation mode. +if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then + # install mode needs the following variables: + generated_by_libtool_version='2.4.2' + notinst_deplibs=' ./libqqwing.la' +else + # When we are sourced in execute mode, $file and $ECHO are already set. + if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then + file="$0" + +# A function that is used when there is no print builtin or printf. +func_fallback_echo () +{ + eval 'cat <<_LTECHO_EOF +$1 +_LTECHO_EOF' +} + ECHO="printf %s\\n" + fi + +# Very basic option parsing. These options are (a) specific to +# the libtool wrapper, (b) are identical between the wrapper +# /script/ and the wrapper /executable/ which is used only on +# windows platforms, and (c) all begin with the string --lt- +# (application programs are unlikely to have options which match +# this pattern). +# +# There are only two supported options: --lt-debug and +# --lt-dump-script. There is, deliberately, no --lt-help. +# +# The first argument to this parsing function should be the +# script's ./libtool value, followed by no. +lt_option_debug= +func_parse_lt_options () +{ + lt_script_arg0=$0 + shift + for lt_opt + do + case "$lt_opt" in + --lt-debug) lt_option_debug=1 ;; + --lt-dump-script) + lt_dump_D=`$ECHO "X$lt_script_arg0" | /usr/bin/sed -e 's/^X//' -e 's%/[^/]*$%%'` + test "X$lt_dump_D" = "X$lt_script_arg0" && lt_dump_D=. + lt_dump_F=`$ECHO "X$lt_script_arg0" | /usr/bin/sed -e 's/^X//' -e 's%^.*/%%'` + cat "$lt_dump_D/$lt_dump_F" + exit 0 + ;; + --lt-*) + $ECHO "Unrecognized --lt- option: '$lt_opt'" 1>&2 + exit 1 + ;; + esac + done + + # Print the debug banner immediately: + if test -n "$lt_option_debug"; then + echo "qqwing:qqwing:${LINENO}: libtool wrapper (GNU libtool) 2.4.2 Debian-2.4.2-1.10ubuntu1" 1>&2 + fi +} + +# Used when --lt-debug. Prints its arguments to stdout +# (redirection is the responsibility of the caller) +func_lt_dump_args () +{ + lt_dump_args_N=1; + for lt_arg + do + $ECHO "qqwing:qqwing:${LINENO}: newargv[$lt_dump_args_N]: $lt_arg" + lt_dump_args_N=`expr $lt_dump_args_N + 1` + done +} + +# Core function for launching the target application +func_exec_program_core () +{ + + if test -n "$lt_option_debug"; then + $ECHO "qqwing:qqwing:${LINENO}: newargv[0]: $progdir/$program" 1>&2 + func_lt_dump_args ${1+"$@"} 1>&2 + fi + exec "$progdir/$program" ${1+"$@"} + + $ECHO "$0: cannot exec $program $*" 1>&2 + exit 1 +} + +# A function to encapsulate launching the target application +# Strips options in the --lt-* namespace from $@ and +# launches target application with the remaining arguments. +func_exec_program () +{ + case " $* " in + *\ --lt-*) + for lt_wr_arg + do + case $lt_wr_arg in + --lt-*) ;; + *) set x "$@" "$lt_wr_arg"; shift;; + esac + shift + done ;; + esac + func_exec_program_core ${1+"$@"} +} + + # Parse options + func_parse_lt_options "$0" ${1+"$@"} + + # Find the directory that this script lives in. + thisdir=`$ECHO "$file" | /usr/bin/sed 's%/[^/]*$%%'` + test "x$thisdir" = "x$file" && thisdir=. + + # Follow symbolic links until we get to the real thisdir. + file=`ls -ld "$file" | /usr/bin/sed -n 's/.*-> //p'` + while test -n "$file"; do + destdir=`$ECHO "$file" | /usr/bin/sed 's%/[^/]*$%%'` + + # If there was a directory component, then change thisdir. + if test "x$destdir" != "x$file"; then + case "$destdir" in + [\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;; + *) thisdir="$thisdir/$destdir" ;; + esac + fi + + file=`$ECHO "$file" | /usr/bin/sed 's%^.*/%%'` + file=`ls -ld "$thisdir/$file" | /usr/bin/sed -n 's/.*-> //p'` + done + + # Usually 'no', except on cygwin/mingw when embedded into + # the cwrapper. + WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no + if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then + # special case for '.' + if test "$thisdir" = "."; then + thisdir=`pwd` + fi + # remove .libs from thisdir + case "$thisdir" in + *[\\/].libs ) thisdir=`$ECHO "$thisdir" | /usr/bin/sed 's%[\\/][^\\/]*$%%'` ;; + .libs ) thisdir=. ;; + esac + fi + + # Try to get the absolute directory name. + absdir=`cd "$thisdir" && pwd` + test -n "$absdir" && thisdir="$absdir" + + program='qqwing' + progdir="$thisdir/.libs" + + + if test -f "$progdir/$program"; then + # Add our own library path to LD_LIBRARY_PATH + LD_LIBRARY_PATH="/home/himbeere/Downloads/qqwing-1.3.4/.libs:$LD_LIBRARY_PATH" + + # Some systems cannot cope with colon-terminated LD_LIBRARY_PATH + # The second colon is a workaround for a bug in BeOS R4 sed + LD_LIBRARY_PATH=`$ECHO "$LD_LIBRARY_PATH" | /usr/bin/sed 's/::*$//'` + + export LD_LIBRARY_PATH + + if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then + # Run the actual program with our arguments. + func_exec_program ${1+"$@"} + fi + else + # The program doesn't exist. + $ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2 + $ECHO "This script is just a wrapper for $program." 1>&2 + $ECHO "See the libtool documentation for more information." 1>&2 + exit 1 + fi +fi diff --git a/extensions/fablabchemnitz/sudoku/qqwing.exe b/extensions/fablabchemnitz/sudoku/qqwing.exe new file mode 100644 index 0000000000000000000000000000000000000000..b39426adf04526e9f96c89affdb26f917baec556 GIT binary patch literal 135168 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 literal 0 HcmV?d00001 diff --git a/extensions/fablabchemnitz/sudoku/sudoku.inx b/extensions/fablabchemnitz/sudoku/sudoku.inx new file mode 100644 index 0000000..5a3e671 --- /dev/null +++ b/extensions/fablabchemnitz/sudoku/sudoku.inx @@ -0,0 +1,47 @@ + + + Sudoku + fablabchemnitz.de.sudoku + + + + + + + + + + + 1 + 1 + + + + + + + + + 6 + 1 + + + 255 + 4243148799 + 2290779647 + 3298820351 + 1923076095 + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/sudoku/sudoku.py b/extensions/fablabchemnitz/sudoku/sudoku.py new file mode 100644 index 0000000..051459f --- /dev/null +++ b/extensions/fablabchemnitz/sudoku/sudoku.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +''' +render_sudoku.py +A sudoku generator plugin for Inkscape, but also can be used as a standalone +command line application. + +Copyright (C) 2011 Chris Savery + +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 +''' + +__version__ = "0.1" + +import inkex +import os +import subprocess +from lxml import etree +from inkex import Color + +class Sudoku(inkex.EffectExtension): + + def add_arguments(self, pars): + pars.add_argument("--tab") + pars.add_argument("--difficulty",default="mixed", help='How difficult to make puzzles.') + pars.add_argument("--rows", type=int, default=1, help='Number of puzzle rows.') + pars.add_argument("--cols", type=int, default=1, help='Number of puzzle columns.') + pars.add_argument("--puzzle_size", type=int, default=6, help='The width & height of each puzzle.') + pars.add_argument("--puzzle_gap", type=int, default=1, help='The space between puzzles.') + pars.add_argument("--color_text", type=Color, default=255, help='Color for given numbers.') + pars.add_argument("--color_bkgnd", type=Color, default=4243148799, help='Color for the puzzle background.') + pars.add_argument("--color_puzzle",type=Color, default=2290779647, help='Border color for the puzzles.') + pars.add_argument("--color_boxes", type=Color, default=3298820351, help='Border color for puzzle boxes.') + pars.add_argument("--color_cells", type=Color, default=1923076095, help='Border color for the puzzle cells.') + pars.add_argument("--units", help="The unit of the dimensions") + + def draw_grid(self, g_puz, x, y): + bkgnd_style = {'stroke':'none', 'stroke-width':'2', 'fill':self.options.color_bkgnd } + puzzle_style = {'stroke':self.options.color_puzzle, 'stroke-width':'2', 'fill':'none' } + boxes_style = {'stroke':self.options.color_boxes, 'stroke-width':'2', 'fill':'none' } + cells_style = {'stroke':self.options.color_cells, 'stroke-width':'1', 'fill':'none' } + g = etree.SubElement(g_puz, 'g') + self.draw_rect(g, bkgnd_style, self.left+x, self.top+y, self.size, self.size) + self.draw_rect(g, cells_style, self.left+x+self.size/9, self.top+y, self.size/9, self.size) + self.draw_rect(g, cells_style, self.left+x+self.size/3+self.size/9, self.top+y, self.size/9, self.size) + self.draw_rect(g, cells_style, self.left+x+2*self.size/3+self.size/9, self.top+y, self.size/9, self.size) + self.draw_rect(g, cells_style, self.left+x, self.top+y+self.size/9, self.size, self.size/9) + self.draw_rect(g, cells_style, self.left+x, self.top+y+self.size/3+self.size/9, self.size, self.size/9) + self.draw_rect(g, cells_style, self.left+x, self.top+y+2*self.size/3+self.size/9, self.size, self.size/9) + self.draw_rect(g, boxes_style, self.left+x+self.size/3, self.top+y, self.size/3, self.size) + self.draw_rect(g, boxes_style, self.left+x, self.top+y+self.size/3, self.size, self.size/3) + self.draw_rect(g, puzzle_style, self.left+x, self.top+y, self.size, self.size) + + def draw_rect(self, g, style, x, y, w, h): + attribs = {'style':str(inkex.Style(style)), 'x':str(x), 'y':str(y), 'height':str(h), 'width':str(w) } + etree.SubElement(g, inkex.addNS('rect','svg'), attribs) + + def fill_puzzle(self, g_puz, x, y, data): + cellsize = self.size / 9 + txtsize = self.size / 12 + offset = (cellsize + txtsize)/2.25 + g = etree.SubElement(g_puz, 'g') + text_style = {'font-size':str(txtsize), + 'fill':self.options.color_text, + 'font-family':'arial', + 'text-anchor':'middle', 'text-align':'center' } + #inkex.utils.debug(len(data)) + for n in range(len(data)): + #inkex.utils.debug(str(n)) + #inkex.utils.debug("data["+str(n)+"]="+str(data[n])) + if str(data[n]) in "123456789": + attribs = {'style': str(inkex.Style(text_style)), + 'x': str(self.left + x + n%9 * cellsize + cellsize/2 ), 'y': str(self.top + y + n//9 * cellsize + offset ) } + etree.SubElement(g, 'text', attribs).text = str(data[n]) + + + def effect(self): + extension_dir = os.path.dirname(os.path.realpath(__file__)) + if os.name == "nt": + qqwing = os.path.join(extension_dir, "qqwing.exe") + else: + qqwing = os.path.join(extension_dir, "qqwing") + args = [qqwing, "--one-line", "--generate", str(self.options.rows * self.options.cols)] + if self.options.difficulty != 'mixed': + args.extend(["--difficulty", self.options.difficulty]) + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as qqwing_process: + data = qqwing_process.communicate() + #inkex.utils.debug(data) + data_processed = data[0].decode('UTF-8').splitlines() + + parent = self.document.getroot() + self.doc_w = self.svg.unittouu(parent.get('width')) + self.doc_h = self.svg.unittouu(parent.get('height')) + self.size = self.svg.unittouu(str(self.options.puzzle_size) + self.options.units) + self.gap = self.svg.unittouu(str(self.options.puzzle_gap) + self.options.units) + self.shift = self.size + self.gap + self.left = (self.doc_w - (self.options.cols * self.shift - self.gap))/2 + self.top = (self.doc_h - (self.options.rows * self.shift - self.gap))/2 + self.sudoku_g = etree.SubElement(parent, 'g', {'id':'sudoku'}) + for row in range(0, self.options.rows): + for col in range(0, self.options.cols): + g = etree.SubElement(self.sudoku_g, 'g', {'id':'puzzle_'+str(col)+str(row)}) + self.draw_grid(g, col*self.shift, row*self.shift) + self.fill_puzzle(g, col*self.shift, row*self.shift, data_processed[col+row*self.options.cols]) + +if __name__ == '__main__': + Sudoku().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/visicut/meta.json b/extensions/fablabchemnitz/visicut/meta.json index f1b1134..fcd34bd 100644 --- a/extensions/fablabchemnitz/visicut/meta.json +++ b/extensions/fablabchemnitz/visicut/meta.json @@ -9,13 +9,13 @@ "license": "GNU LGPL v3", "license_url": "https://github.com/t-oster/VisiCut/blob/master/LICENSE", "comment": "", - "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/visicut", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/visicut", "fork_url": "https://github.com/t-oster/VisiCut/tree/master/tools/inkscape_extension", "documentation_url": "https://stadtfabrikanten.org/display/IFM/Open+in+VisiCut", "inkscape_gallery_url": null, "main_authors": [ "github.com/t-oster", - "github.com/vmario89" + "github.com/eridur-de" ] } ] \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/meta.json b/extensions/fablabchemnitz/vpypetools/meta.json new file mode 100644 index 0000000..68b831e --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/meta.json @@ -0,0 +1,20 @@ +[ + { + "name": "", + "id": "fablabchemnitz.de.vpype_", + "path": "vpypetools", + "dependent_extensions": null, + "original_name": "vpypetools", + "original_id": "fablabchemnitz.de.vpype_", + "license": "GNU GPL v3", + "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE", + "comment": "Created by Mario Voigt", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/vpypetools", + "fork_url": null, + "documentation_url": "https://stadtfabrikanten.org/display/IFM/vpypetools", + "inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85vpypetools-vpype-for-inkscape", + "main_authors": [ + "github.com/eridur-de" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpype_logo.svg b/extensions/fablabchemnitz/vpypetools/vpype_logo.svg new file mode 100644 index 0000000..2723d3c --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpype_logo.svg @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools.py b/extensions/fablabchemnitz/vpypetools/vpypetools.py new file mode 100644 index 0000000..f470d95 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 + +import logging +logger = logging.getLogger() +logger.setLevel(level=logging.ERROR) #we set this to error before importing vpype to ignore the nasty output "WARNING:root:!!! `vpype.Length` is deprecated, use `vpype.LengthType` instead." + +import sys +import os +from lxml import etree + +import inkex +from inkex import transforms, bezier, PathElement +from inkex.paths import CubicSuperPath, Path +from inkex.command import inkscape + +import vpype +import vpype_viewer +from vpype_viewer import ViewMode +from vpype_cli import execute + +logger = logging.getLogger() +logger.setLevel(level=logging.WARNING) #after importing vpype we enabled logging again + +import warnings # we import this to suppress moderngl warnings from vpype_viewer + +from shapely.geometry import LineString, Point + +""" +Extension for InkScape 1.X +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 02.04.2021 +Last patch: 06.06.2021 +License: GNU GPL v3 + +This piece of spaghetti-code, called "vpypetools", is a wrapper to pass (pipe) line elements from InkScape selection (or complete canvas) to vpype. +It allows to run basic commands on the geometry. The converted lines are getting pushed back into InkScape. +vpypetools allows to enable some important adjusters and debugging settings to get the best out of it. + +vpypetools is based on + - Aaron Spike's "Flatten Bezier" extension, licensed by GPL v2 + - a lot of other extensions to rip off the required code pieces ;-) + - used (tested) version of vpype: commit id https://github.com/abey79/vpype/commit/0b0dc8dd7e32998dbef639f9db578c3bff02690b (29.03.2021) + - used (tested) version of vpype occult: commit id https://github.com/LoicGoulefert/occult/commit/2d04ca57d69078755c340066c226fd6cd927d41e (04.02.2021) + +CLI / API docs: +- https://vpype.readthedocs.io/en/stable/api/vpype_cli.html#module-vpype_cli +- https://vpype.readthedocs.io/en/stable/api/vpype.html#module-vpype + +Todo's +- find some python code to auto-convert strokes and objects to paths (for input and for output again) +- remove fill property of converted lines (because there is no fill anymore) without crashing Inkscape ... +- as we use flatten() we modify the original path. rewrite to avoid modifications to original path +""" + +class vpypetools (inkex.EffectExtension): + + def add_arguments(self, pars): + + # Line Sorting + pars.add_argument("--linesort", type=inkex.Boolean, default=False) + pars.add_argument("--linesort_no_flip", type=inkex.Boolean, default=False, help="Disable reversing stroke direction for optimization") + + # Line Merging + pars.add_argument("--linemerge", type=inkex.Boolean, default=False) + pars.add_argument("--linemerge_tolerance", type=float, default=0.500, help="Maximum distance between two line endings that should be merged (default 0.500 mm)") + pars.add_argument("--linemerge_no_flip", type=inkex.Boolean, default=False, help="Disable reversing stroke direction for merging") + + # Trimming + pars.add_argument("--trim", type=inkex.Boolean, default=False) + pars.add_argument("--trim_x_margin", type=float, default=0.000, help="trim margin - x direction (mm)") # keep default at 0.000 to keep clean bbox + pars.add_argument("--trim_y_margin", type=float, default=0.000, help="trim margin - y direction (mm)") # keep default at 0.000 to keep clean bbox + + # Relooping + pars.add_argument("--reloop", type=inkex.Boolean, default=False) + pars.add_argument("--reloop_tolerance", type=float, default=0.500, help="Controls how close the path beginning and end must be to consider it closed (default 0.500 mm)") + + # Multipass + pars.add_argument("--multipass", type=inkex.Boolean, default=False) + pars.add_argument("--multipass_count", type=int, default=2, help="How many passes for each line (default 2)") + + # Filter + pars.add_argument("--filter", type=inkex.Boolean, default=False) + pars.add_argument("--filter_tolerance", type=float, default=0.050, help="Tolerance used to determined if a line is closed or not (default 0.050 mm)") + pars.add_argument("--filter_closed", type=inkex.Boolean, default=False, help="Keep closed lines") + pars.add_argument("--filter_not_closed", type=inkex.Boolean, default=False, help="Keep open lines") + pars.add_argument("--filter_min_length_enabled", type=inkex.Boolean, default=False, help="filter by min length") + pars.add_argument("--filter_min_length", type=float, default=0.000, help="Keep lines whose length isn't shorter than value") + pars.add_argument("--filter_max_length_enabled", type=inkex.Boolean, default=False, help="filter by max length") + pars.add_argument("--filter_max_length", type=float, default=0.000, help="Keep lines whose length isn't greater than value") + + # Split All + pars.add_argument("--splitall", type=inkex.Boolean, default=False) + + # Plugin Occult + pars.add_argument("--plugin_occult", type=inkex.Boolean, default=False) + pars.add_argument("--plugin_occult_tolerance", type=float, default=0.01, help="Max distance between start and end point to consider a path closed (default 0.01 mm)") + pars.add_argument("--plugin_occult_keepseparatelayer", type=inkex.Boolean, default=False, help="Put occulted lines to separate layer") + + # Plugin Deduplicate + pars.add_argument("--plugin_deduplicate", type=inkex.Boolean, default=False) + pars.add_argument("--plugin_deduplicate_tolerance", type=float, default=0.01, help="Max distance between points to consider them equal (default 0.01 mm)") + pars.add_argument("--plugin_deduplicate_keepseparatelayer", type=inkex.Boolean, default=False, help="Put duplicate lines to separate layer") + + # Free Mode + pars.add_argument("--tab") + pars.add_argument("--freemode", type=inkex.Boolean, default=False) + pars.add_argument("--freemode_cmd1", default="") + pars.add_argument("--freemode_cmd1_enabled", type=inkex.Boolean, default=True) + pars.add_argument("--freemode_cmd2", default="") + pars.add_argument("--freemode_cmd2_enabled", type=inkex.Boolean, default=False) + pars.add_argument("--freemode_cmd3", default="") + pars.add_argument("--freemode_cmd3_enabled", type=inkex.Boolean, default=False) + pars.add_argument("--freemode_cmd4", default="") + pars.add_argument("--freemode_cmd4_enabled", type=inkex.Boolean, default=False) + pars.add_argument("--freemode_cmd5", default="") + pars.add_argument("--freemode_cmd5_enabled", type=inkex.Boolean, default=False) + pars.add_argument("--freemode_show_cmd", type=inkex.Boolean, default=False) + + # General Settings + pars.add_argument("--input_handling", default="paths", help="Input handling") + pars.add_argument("--flattenbezier", type=inkex.Boolean, default=False, help="Flatten bezier curves to polylines") + pars.add_argument("--flatness", type=float, default=0.1, help="Minimum flatness = 0.1. The smaller the value the more fine segments you will get (quantization).") + pars.add_argument("--decimals", type=int, default=3, help="Accuracy for imported lines' coordinates into vpype. Does not work for 'Multilayer/document'") + pars.add_argument("--simplify", type=inkex.Boolean, default=False, help="Reduces significantly the number of segments used to approximate the curve while still guaranteeing an accurate conversion, but may increase the execution time. Does not work for 'Singlelayer/paths'") + pars.add_argument("--parallel", type=inkex.Boolean, default=False, help="Enables multiprocessing for the SVG conversion. This is recommended ONLY when using 'Simplify geometry' on large SVG files with many curved elements. Does not work for 'Singlelayer/paths'") + pars.add_argument("--output_show", type=inkex.Boolean, default=False, help="This will open a separate window showing the finished SVG data. If enabled, output is not applied to InkScape canvas (only for preview)!") + pars.add_argument("--output_show_points", type=inkex.Boolean, default=False, help="Enable point display in viewer") + pars.add_argument("--output_stats", type=inkex.Boolean, default=False, help="Show output statistics before/after conversion") + pars.add_argument("--output_trajectories", type=inkex.Boolean, default=False, help="Add paths for the travel trajectories") + pars.add_argument("--keep_objects", type=inkex.Boolean, default=False, help="If false, selected paths will be removed") + pars.add_argument("--strokes_to_paths", type=inkex.Boolean, default=True, help="Recommended option. Performs 'Path' > 'Stroke to Path' (CTRL + ALT + C) to convert vpype converted lines back to regular path objects") + pars.add_argument("--use_style_of_first_element", type=inkex.Boolean, default=True, help="If enabled the first element in selection is scanned and we apply it's style to all imported vpype lines (but not for trajectories). Does not work for 'Multilayer/document'") + pars.add_argument("--lines_stroke_width", type=float, default=1.0, help="Stroke width of tooling lines (px). Gets overwritten if 'Use style of first selected element' is enabled") + pars.add_argument("--trajectories_stroke_width", type=float, default=1.0, help="Stroke width of trajectory lines (px). Gets overwritten if 'Use style of first selected element' is enabled") + + def effect(self): + lc = vpype.LineCollection() # create a new array of LineStrings consisting of Points. We convert selected paths to polylines and grab their points + elementsToWork = [] # we make an array of all collected nodes to get the boundingbox of that array. We need it to place the vpype converted stuff to the correct XY coordinates + + def flatten(node): + #path = node.path.to_superpath() + path = node.path.transform(node.composed_transform()).to_superpath() + bezier.cspsubdiv(path, self.options.flatness) + newpath = [] + for subpath in path: + first = True + for csp in subpath: + cmd = 'L' + if first: + cmd = 'M' + first = False + newpath.append([cmd, [csp[1][0], csp[1][1]]]) + node.path = newpath + + # flatten the node's path to linearize, split up the path to it's subpaths (break apart) and add all points to the vpype lines collection + def convertPath(node, nodes = None): + if nodes is None: + nodes = [] + if node.tag == inkex.addNS('path','svg'): + nodes.append(node) + if self.options.flattenbezier is True: + flatten(node) + + raw = node.path.to_arrays() + subPaths, prev = [], 0 + for i in range(len(raw)): # Breaks compound paths into simple paths + if raw[i][0] == 'M' and i != 0: + subPaths.append(raw[prev:i]) + prev = i + subPaths.append(raw[prev:]) + for subPath in subPaths: + points = [] + for csp in subPath: + if len(csp[1]) > 0: #we need exactly two points per straight line segment + points.append(Point(round(csp[1][0], self.options.decimals), round(csp[1][1], self.options.decimals))) + if len(subPath) > 2 and (subPath[-1][0] == 'Z' or subPath[0][1] == subPath[-1][1]): #check if path has more than 2 points and is closed by Z or first pont == last point + points.append(Point(round(subPath[0][1][0], self.options.decimals), round(subPath[0][1][1], self.options.decimals))) #if closed, we add the first point again + lc.append(LineString(points)) + + children = node.getchildren() + if children is not None: + for child in children: + convertPath(child, nodes) + return nodes + + doc = None #create a vpype document + + ''' + if 'paths' we process paths only. Objects like rectangles or strokes like polygon have to be converted before accessing them + if 'layers' we can process all layers in the complete document + ''' + if self.options.input_handling == "paths": + # getting the bounding box of the current selection. We use to calculate the offset XY from top-left corner of the canvas. This helps us placing back the elements + input_bbox = None + if len(self.svg.selected) == 0: + elementsToWork = convertPath(self.document.getroot()) + for element in elementsToWork: + input_bbox += element.bounding_box() + else: + elementsToWork = None + for element in self.svg.selected.values(): + elementsToWork = convertPath(element, elementsToWork) + #input_bbox = inkex.elements._selected.ElementList.bounding_box(self.svg.selected) # get BoundingBox for selection + input_bbox = self.svg.selection.bounding_box() # get BoundingBox for selection + if len(lc) == 0: + self.msg('Selection appears to be empty or does not contain any valid svg:path nodes. Try to cast your objects to paths using CTRL + SHIFT + C or strokes to paths using CTRL + ALT+ C') + return + # find the first object in selection which has a style attribute (skips groups and other things which have no style) + firstElementStyle = None + for element in elementsToWork: + if element.attrib.has_key('style'): + firstElementStyle = element.get('style') + doc = vpype.Document(page_size=(input_bbox.width + input_bbox.left, input_bbox.height + input_bbox.top)) #create new vpype document + doc.add(lc, layer_id=None) # we add the lineCollection (converted selection) to the vpype document + + elif self.options.input_handling == "layers": + doc = vpype.read_multilayer_svg(self.options.input_file, quantization = self.options.flatness, crop = False, simplify = self.options.simplify, parallel = self.options.parallel, default_width = self.document.getroot().get('width'), default_height = self.document.getroot().get('height')) + + for element in self.document.getroot().xpath("//svg:g", namespaces=inkex.NSS): #all groups/layers + elementsToWork.append(element) + + tooling_length_before = doc.length() + traveling_length_before = doc.pen_up_length() + + # build and execute the conversion command + # the following code block is not intended to sum up the commands to build a series (pipe) of commands! + ########################################## + + # Line Sorting + if self.options.linesort is True: + command = "linesort " + if self.options.linesort_no_flip is True: + command += " --no-flip" + + # Line Merging + if self.options.linemerge is True: + command = "linemerge --tolerance " + str(self.options.linemerge_tolerance) + if self.options.linemerge_no_flip is True: + command += " --no-flip" + + # Trimming + if self.options.trim is True: + command = "trim " + str(self.options.trim_x_margin) + " " + str(self.options.trim_y_margin) + + # Relooping + if self.options.reloop is True: + command = "reloop --tolerance " + str(self.options.reloop_tolerance) + + # Multipass + if self.options.multipass is True: + command = "multipass --count " + str(self.options.multipass_count) + + # Filter + if self.options.filter is True: + command = "filter --tolerance " + str(self.options.filter_tolerance) + if self.options.filter_min_length_enabled is True: + command += " --min-length " + str(self.options.filter_min_length) + if self.options.filter_max_length_enabled is True: + command += " --max-length " + str(self.options.filter_max_length) + if self.options.filter_closed is True and self.options.filter_not_closed is False: + command += " --closed" + if self.options.filter_not_closed is True and self.options.filter_closed is False: + command += " --not-closed" + if self.options.filter_closed is False and \ + self.options.filter_not_closed is False and \ + self.options.filter_min_length_enabled is False and \ + self.options.filter_max_length_enabled is False: + self.msg('No filters to apply. Please select at least one filter.') + return + + # Plugin Occult + if self.options.plugin_occult is True: + command = "occult --tolerance " + str(self.options.plugin_occult_tolerance) + if self.options.plugin_occult_keepseparatelayer is True: + command += " --keep-occulted" + + # Plugin Deduplicate + if self.options.plugin_deduplicate is True: + command = "deduplicate --tolerance " + str(self.options.plugin_deduplicate_tolerance) + if self.options.plugin_deduplicate_keepseparatelayer is True: + command += " --keep-duplicates" + + # Split All + if self.options.splitall is True: + command = " splitall" + + # Free Mode + if self.options.freemode is True: + command = "" + if self.options.freemode_cmd1_enabled is True: + command += " " + self.options.freemode_cmd1.strip() + if self.options.freemode_cmd2_enabled is True: + command += " " + self.options.freemode_cmd2.strip() + if self.options.freemode_cmd3_enabled is True: + command += " " + self.options.freemode_cmd3.strip() + if self.options.freemode_cmd4_enabled is True: + command += " " + self.options.freemode_cmd4.strip() + if self.options.freemode_cmd5_enabled is True: + command += " " + self.options.freemode_cmd5.strip() + if self.options.freemode_cmd1_enabled is False and \ + self.options.freemode_cmd2_enabled is False and \ + self.options.freemode_cmd3_enabled is False and \ + self.options.freemode_cmd4_enabled is False and \ + self.options.freemode_cmd5_enabled is False: + self.msg("Warning: empty vpype pipeline. With this you are just getting read-write layerset/lineset.") + else: + if self.options.freemode_show_cmd is True: + self.msg("Your command pipe will be the following:") + self.msg(command) + + # self.msg(command) + try: + warnings.filterwarnings('ignore', 'SelectableGroups dict interface') + doc = execute(command, doc) + except Exception as e: + self.msg("Error in vpype:\n" + str(e)) + return + + ########################################## + + tooling_length_after = doc.length() + traveling_length_after = doc.pen_up_length() + if tooling_length_before > 0: + tooling_length_saving = (1.0 - tooling_length_after / tooling_length_before) * 100.0 + else: + tooling_length_saving = 0.0 + if traveling_length_before > 0: + traveling_length_saving = (1.0 - traveling_length_after / traveling_length_before) * 100.0 + else: + traveling_length_saving = 0.0 + if self.options.output_stats is True: + self.msg('Total tooling length before vpype conversion: ' + str('{:0.2f}'.format(tooling_length_before)) + ' mm') + self.msg('Total traveling length before vpype conversion: ' + str('{:0.2f}'.format(traveling_length_before)) + ' mm') + self.msg('Total tooling length after vpype conversion: ' + str('{:0.2f}'.format(tooling_length_after)) + ' mm') + self.msg('Total traveling length after vpype conversion: ' + str('{:0.2f}'.format(traveling_length_after)) + ' mm') + self.msg('Total tooling length optimized: ' + str('{:0.2f}'.format(tooling_length_saving)) + ' %') + self.msg('Total traveling length optimized: ' + str('{:0.2f}'.format(traveling_length_saving)) + ' %') + + if tooling_length_after == 0: + self.msg('No lines left after vpype conversion. Conversion result is empty. Cannot continue. Check your document about containing any svg:path elements. You will need to convert objects and strokes to paths first! Vpype command chain was:') + self.msg(command) + return + + # show the vpype document visually + if self.options.output_show: + warnings.filterwarnings("ignore") # workaround to suppress annoying DeprecationWarning + # vpype_viewer.show(doc, view_mode=ViewMode.PREVIEW, show_pen_up=self.options.output_trajectories, show_points=self.options.output_show_points, pen_width=0.1, pen_opacity=1.0, argv=None) + vpype_viewer.show(doc, view_mode=ViewMode.PREVIEW, show_pen_up=self.options.output_trajectories, show_points=self.options.output_show_points, argv=None) # https://vpype.readthedocs.io/en/stable/api/vpype_viewer.ViewMode.html + warnings.filterwarnings("default") # reset warning filter + exit(0) #we leave the code loop because we only want to preview. We don't want to import the geometry + + # save the vpype document to new svg file and close it afterwards + output_file = self.options.input_file + ".vpype.svg" + output_fileIO = open(output_file, "w", encoding="utf-8") + #vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer', single_path = True) + vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer') + #vpype.write_svg(output_fileIO, doc, page_size=(self.svg.unittouu(self.document.getroot().get('width')), self.svg.unittouu(self.document.getroot().get('height'))), center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer') + output_fileIO.close() + + # parse the SVG file + try: + stream = open(output_file, 'r') + except FileNotFoundError as e: + self.msg("There was no SVG output generated by vpype. Cannot continue") + exit(1) + p = etree.XMLParser(huge_tree=True) + import_doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True)) + stream.close() + + # handle pen_up trajectories (travel lines) + trajectoriesLayer = import_doc.getroot().xpath("//svg:g[@id='pen_up_trajectories']", namespaces=inkex.NSS) + if self.options.output_trajectories is True: + if len(trajectoriesLayer) > 0: + trajectoriesLayer[0].set('style', 'stroke:#0000ff;stroke-width:{:0.2f}px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none'.format(self.options.trajectories_stroke_width)) + trajectoriesLayer[0].attrib.pop('stroke') # remove unneccesary stroke attribute + trajectoriesLayer[0].attrib.pop('fill') # remove unneccesary fill attribute + else: + if len(trajectoriesLayer) > 0: + trajectoriesLayer[0].delete() + + lineLayers = import_doc.getroot().xpath("//svg:g[not(@id='pen_up_trajectories')]", namespaces=inkex.NSS) #all layer except the pen_up trajectories layer + if self.options.use_style_of_first_element is True and self.options.input_handling == "paths" and firstElementStyle is not None: + + # if we remove the fill property and use "Use style of first element in layer" the conversion will just crash with an unknown reason + #declarations = firstElementStyle.split(';') + #for i, decl in enumerate(declarations): + # parts = decl.split(':', 2) + # if len(parts) == 2: + # (prop, val) = parts + # prop = prop.strip().lower() + # #if prop == 'fill': + # # declarations[i] = prop + ':none' + for lineLayer in lineLayers: + #lineLayer.set('style', ';'.join(declarations)) + lineLayer.set('style', firstElementStyle) + lineLayer.attrib.pop('stroke') # remove unneccesary stroke attribute + lineLayer.attrib.pop('fill') # remove unneccesary fill attribute + + else: + for lineLayer in lineLayers: + if lineLayer.attrib.has_key('stroke'): + color = lineLayer.get('stroke') + lineLayer.set('style', 'stroke:' + color + ';stroke-width:{:0.2f}px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none'.format(self.options.lines_stroke_width)) + lineLayer.attrib.pop('stroke') # remove unneccesary stroke attribute + lineLayer.attrib.pop('fill') # remove unneccesary fill attribute + + import_viewBox = import_doc.getroot().get('viewBox').split(" ") + self_viewBox = self.document.getroot().get('viewBox') + + if self_viewBox is not None: #some SVG files do not have this attribute + self_viewBoxValues = self_viewBox.split(" ") + scaleX = self.svg.unittouu(self_viewBoxValues[2]) / self.svg.unittouu(import_viewBox[2]) + scaleY = self.svg.unittouu(self_viewBoxValues[3]) / self.svg.unittouu(import_viewBox[3]) + + for element in import_doc.getroot().iter("{http://www.w3.org/2000/svg}g"): + e = self.document.getroot().append(element) + if self.options.input_handling == "layers": + if self_viewBox is not None: + element.set('transform', 'scale(' + str(scaleX) + ',' + str(scaleY) + ')') #imported groups need to be transformed. Or they have wrong size. Reason: different viewBox sizes/units in namedview definitions + + # convert vpype polylines/lines/polygons to regular paths again (objects to paths) + if self.options.strokes_to_paths is True: + for line in element.iter("{http://www.w3.org/2000/svg}line"): + newLine = PathElement() + newLine.path = Path("M {},{} L {},{}".format(line.attrib['x1'], line.attrib['y1'], line.attrib['x2'], line.attrib['y2'])) + element.append(newLine) + line.delete() + + for polyline in element.iter("{http://www.w3.org/2000/svg}polyline"): + newPolyLine = PathElement() + newPolyLine.path = Path('M' + polyline.attrib['points']) + element.append(newPolyLine) + polyline.delete() + + for polygon in element.iter("{http://www.w3.org/2000/svg}polygon"): + newPolygon = PathElement() + #newPolygon.path = Path('M' + " ".join(polygon.attrib['points'].split(' ')[:-1]) + ' Z') #remove the last point of the points string by splitting at whitespace, converting to array and removing the last item. then converting back to string + newPolygon.path = Path('M' + " ".join(polygon.attrib['points'].split(' ')) + ' Z') + element.append(newPolygon) + polygon.delete() + + # Delete the temporary file again because we do not need it anymore + if os.path.exists(output_file): + os.remove(output_file) + + # Remove selection objects to do a real replace with new objects from vpype document + if self.options.keep_objects is False: + for element in elementsToWork: + element.delete() + +if __name__ == '__main__': + vpypetools().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_deduplicate.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_deduplicate.inx new file mode 100644 index 0000000..fb22b8c --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_deduplicate.inx @@ -0,0 +1,78 @@ + + + Deduplicate Plugin + fablabchemnitz.de.vpype_plugin_deduplicate + + + vpype_logo.svg + + true + 0.01 + false + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_filter.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_filter.inx new file mode 100644 index 0000000..a0dbd58 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_filter.inx @@ -0,0 +1,83 @@ + + + Filter + fablabchemnitz.de.vpype_filter + + + vpype_logo.svg + + true + 0.050 + false + false + false + 0.000 + false + 0.000 + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_freemode.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_freemode.inx new file mode 100644 index 0000000..b670dee --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_freemode.inx @@ -0,0 +1,113 @@ + + + vpype Free Mode + fablabchemnitz.de.vpype_free_mode + + + vpype_logo.svg + + true + layout 1024x768 translate 150px 50px + true + scaleto -o 0 0 330 200 + false + rotate 30.0 + false + skew 45.0 60.0 + false + splitall + false + + false + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_linemerge.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_linemerge.inx new file mode 100644 index 0000000..e03411d --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_linemerge.inx @@ -0,0 +1,78 @@ + + + Line Merging (Combine Paths) + fablabchemnitz.de.vpype_linemerging + + + vpype_logo.svg + + true + 0.500 + false + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_linesort.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_linesort.inx new file mode 100644 index 0000000..21ea8bb --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_linesort.inx @@ -0,0 +1,77 @@ + + + Line Sorting + fablabchemnitz.de.vpype_linesorting + + + vpype_logo.svg + + true + false + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_multipass.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_multipass.inx new file mode 100644 index 0000000..76be37d --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_multipass.inx @@ -0,0 +1,77 @@ + + + Multipass + fablabchemnitz.de.vpype_multipass + + + vpype_logo.svg + + true + 2 + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_occult.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_occult.inx new file mode 100644 index 0000000..e959b77 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_occult.inx @@ -0,0 +1,78 @@ + + + Occult Plugin (HLR) + fablabchemnitz.de.vpype_plugin_occult + + + vpype_logo.svg + + true + 0.01 + false + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_relooping.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_relooping.inx new file mode 100644 index 0000000..ebff6d6 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_relooping.inx @@ -0,0 +1,77 @@ + + + Relooping + fablabchemnitz.de.vpype_relooping + + + vpype_logo.svg + + true + 0.500 + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_splitall.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_splitall.inx new file mode 100644 index 0000000..6fbd060 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_splitall.inx @@ -0,0 +1,76 @@ + + + Split All (Break Paths) + fablabchemnitz.de.vpype_splitall + + + vpype_logo.svg + + true + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools_trim.inx b/extensions/fablabchemnitz/vpypetools/vpypetools_trim.inx new file mode 100644 index 0000000..54f8fe8 --- /dev/null +++ b/extensions/fablabchemnitz/vpypetools/vpypetools_trim.inx @@ -0,0 +1,78 @@ + + + Trimming + fablabchemnitz.de.vpype_trimming + + + vpype_logo.svg + + true + 0.000 + 0.000 + + + + + + + + true + 0.100 + 3 + false + false + + + false + false + false + + + false + false + true + true + 1.000 + 1.000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + + + \ No newline at end of file