diff --git a/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.inx b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.inx
new file mode 100644
index 0000000..78a8b70
--- /dev/null
+++ b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.inx
@@ -0,0 +1,91 @@
+
+
+ Cutting Optimizer (Nesting)
+ fablabchemnitz.de.cutting_optimizer
+
+
+
+
+
+
+
+
+ 0.00
+ 1000
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ true
+ 0.00
+ true
+ false
+ 0.00
+
+
+
+ false
+
+ true
+
+ false
+ false
+ false
+ false
+
+ false
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../000_about_fablabchemnitz.svg
+
+
+
+ all
+
+
+
+
+
+
+
+
diff --git a/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py
new file mode 100644
index 0000000..3bd5384
--- /dev/null
+++ b/extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+
+"""
+Extension for InkScape 1.2
+
+CutOptim OS Wrapper script to make CutOptim work on Windows and Linux systems without duplicating .inx files
+
+Author: Mario Voigt / FabLab Chemnitz
+Mail: mario.voigt@stadtfabrikanten.org
+Date: 31.08.2020
+Last patch: 03.11.2022
+License: GNU GPL v3
+
+"""
+
+import inkex
+import sys
+import re
+import os
+import subprocess
+from lxml import etree
+from copy import deepcopy
+import tempfile
+from inkex.command import inkscape, inkscape_command
+
+class CuttingOptimizer(inkex.EffectExtension):
+
+ def openDebugFile(self, file):
+ DETACHED_PROCESS = 0x00000008
+ if os.name == 'nt':
+ subprocess.Popen(["explorer", file], close_fds=True, creationflags=DETACHED_PROCESS, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
+ else:
+ subprocess.Popen(["xdg-open", file], close_fds=True, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
+
+ def add_arguments(self, pars):
+ args = sys.argv[1:]
+ for arg in args:
+ key=arg.split("=")[0]
+ if len(arg.split("=")) == 2:
+ value=arg.split("=")[1]
+ try:
+ if key != "--id":
+ pars.add_argument(key, default=key)
+ except:
+ pass #ignore duplicate id arg
+
+ def effect(self):
+ extension_dir = os.path.dirname(os.path.realpath(__file__))
+ cmd = []
+ if os.name == "nt":
+ cutoptim = os.path.join(extension_dir, "CutOptim.exe")
+ else:
+ cutoptim = os.path.join(extension_dir, "CutOptim")
+ cmd.append(cutoptim)
+
+ 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':
+ continue
+ template.remove(child)
+ group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
+ group.attrib['id'] = 'export_selection_transform'
+ for element in self.svg.selected.values():
+ elem_copy = deepcopy(element)
+ elem_copy.attrib['transform'] = str(element.composed_transform())
+ elem_copy.attrib['style'] = str(element.specified_style())
+ group.append(elem_copy)
+ template.attrib['viewBox'] = self.svg.attrib['viewBox']
+ template.attrib['width'] = self.svg.attrib['width']
+ template.attrib['height'] = self.svg.attrib['height']
+ template.append(group)
+ svg_out = os.path.join(tempfile.gettempdir(), self.svg.get_unique_id("selection") + '.svg')
+ with open(svg_out, 'wb') as fp:
+ fp.write(template.tostring())
+ actions_list=[]
+ actions_list.append("SelectionUnGroup")
+ actions_list.append("export-type:svg")
+ 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
+ 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)
+
+ layerSum = 0
+ layer_output_0 = False
+ cancel_on_error = False
+ for arg in vars(self.options):
+ argval = str(getattr(self.options, arg))
+ if arg not in ("tab", "output", "ids", "selected_nodes", "print_cmd"):
+ #inkex.utils.debug(str(arg) + " = " + str(getattr(self.options, arg)))
+ #fix behaviour of "original" arg which does not correctly gets interpreted if set to false
+ if arg == "original" and argval == "false":
+ continue
+ if arg == "input_file":
+ cmd.append("--file")
+ if len(elements) > 0:
+ cmd.append(svg_out)
+ else:
+ cmd.append(argval)
+ elif arg == "layer_output_0":
+ if argval == "true":
+ layerSum += 0
+ layer_output_0 = True
+ #elif arg == "layer_output_1":
+ # if argval == "true": layerSum += 1
+ elif arg == "layer_output_2":
+ if argval == "true": layerSum += 2
+ elif arg == "layer_output_4":
+ if argval == "true": layerSum += 4
+ elif arg == "layer_output_8":
+ if argval == "true": layerSum += 8
+ elif arg == "layer_output_16":
+ if argval == "true": layerSum += 16
+ elif arg == "cancel_on_error":
+ if argval == "true": cancel_on_error = True
+ else:
+ cmd.append("--{}={}".format(arg, argval))
+ cmd.append("--layer_output")
+ cmd.append("{}".format(layerSum))
+ if layerSum == 0 and layer_output_0 is False:
+ inkex.utils.debug("You need to enable at least one type of layer to continue!")
+ output_file = None
+ if os.name == "nt":
+ output_file = "cutoptim.svg"
+ else:
+ output_file = "/tmp/cutoptim.svg"
+ if os.path.exists(output_file):
+ try:
+ os.remove(output_file)
+ except OSError as e:
+ pass
+
+ cmd.append("--output")
+ cmd.append(output_file)
+
+ # run CutOptim with the parameters provided
+ if self.options.print_cmd == "true":
+ inkex.utils.debug("The following command would be executed on shell:\n")
+ inkex.utils.debug(" ".join(cmd))
+ exit(0)
+ else:
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) as cutoptim:
+ cutoptim.wait()
+ stdout, stderr = cutoptim.communicate()
+ #inkex.utils.debug("stdout:\n{}".format(stdout.decode('UTF-8')))
+ errors = stderr.decode('UTF-8')
+ if len(errors) > 0:
+ inkex.utils.debug("Errors occured:\n{}".format(errors))
+ if len(errors) > 0 and cancel_on_error is True:
+ inkex.utils.debug("Maybe enlarge your document size in case not all polygons could be placed and try again! Nesting was cancelled!")
+ exit(1)
+
+ # check output existence
+ try:
+ stream = open(output_file, 'r')
+ except FileNotFoundError as e:
+ inkex.utils.debug("There was no SVG output generated. Cannot continue. Command was:\n")
+ inkex.utils.debug(" ".join(cmd))
+ exit(1)
+
+ if self.options.original == "false": #we need to use string representation of bool
+ for element in self.document.getroot():
+ if isinstance(element, inkex.ShapeElement):
+ element.delete()
+
+ if self.options.debug_file == "true": #we need to use string representation of bool
+ self.openDebugFile("Debug_CutOptim.txt")
+
+ # write the generated SVG into Inkscape's canvas
+ doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True))
+ stream.close()
+ group = inkex.Group(id="CutOptim")
+ '''
+ 0 = Placed_Layer
+ 1 = Original_Layer (Input Layer) <> see "Keep original layer"
+ 2 = Polygon_Layer
+ 4 = Large_Polygon_Layer
+ 8 = Hull_Placed_Layer
+ 16 = Placed_Polygon_Layer
+ '''
+ if layer_output_0 is False:
+ l0 = None
+ else:
+ l0 = doc.xpath('//svg:g[@inkscape:label="Placed_Layer"]', namespaces=inkex.NSS)
+ #l1 = doc.xpath('//svg:g[@inkscape:label="Original_Layer"]', namespaces=inkex.NSS)
+ l2 = doc.xpath('//svg:g[@inkscape:label="Polygon_Layer"]', namespaces=inkex.NSS)
+ l4 = doc.xpath('//svg:g[@inkscape:label="Large_Polygon_Layer"]', namespaces=inkex.NSS)
+ l8 = doc.xpath('//svg:g[@inkscape:label="Hull_Placed_Layer"]', namespaces=inkex.NSS)
+ l16 = doc.xpath('//svg:g[@inkscape:label="Placed_Polygon_Layer"]', namespaces=inkex.NSS)
+ for layer in (l0, l2, l4, l8, l16): #,l1
+ if layer is not None and len(layer) > 0:
+ for element in layer:#[0].getchildren():
+ group.append(element)
+ self.document.getroot().append(group)
+
+if __name__ == '__main__':
+ CuttingOptimizer().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutting_optimizer/meta.json b/extensions/fablabchemnitz/cutting_optimizer/meta.json
new file mode 100644
index 0000000..1af2561
--- /dev/null
+++ b/extensions/fablabchemnitz/cutting_optimizer/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Cutting Optimizer (Nesting)",
+ "id": "fablabchemnitz.de.cutting_optimizer",
+ "path": "cutting_optimizer",
+ "dependent_extensions": null,
+ "original_name": "Laser Cutting Optmizer",
+ "original_id": "fr.fablab-lannion.inkscape.cutopimiser",
+ "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",
+ "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"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/export_selection_as/export_selection_as.inx b/extensions/fablabchemnitz/export_selection_as/export_selection_as.inx
new file mode 100644
index 0000000..29c517f
--- /dev/null
+++ b/extensions/fablabchemnitz/export_selection_as/export_selection_as.inx
@@ -0,0 +1,72 @@
+
+
+ Export Selection As ...
+ fablabchemnitz.de.export_selection_as
+
+
+ false
+ 1.000
+
+
+
+
+
+
+
+
+ ./inkscape_export/
+ false
+ /usr/share/inkscape/extensions/dxf_outlines.py
+ true
+ false
+ false
+ false
+ 96
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../000_about_fablabchemnitz.svg
+
+
+
+ all
+
+
+
+
+
+ Export selection to separate files.
+
+
+
\ 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
new file mode 100644
index 0000000..cd76043
--- /dev/null
+++ b/extensions/fablabchemnitz/export_selection_as/export_selection_as.py
@@ -0,0 +1,305 @@
+#!/usr/bin/env python3
+
+from copy import deepcopy
+from pathlib import Path
+import logging
+import math
+import os
+import sys
+import subprocess
+from subprocess import Popen, PIPE
+import inkex
+from inkex import Rectangle
+import inkex.command
+from inkex.command import inkscape, inkscape_command
+import tempfile
+from PIL import Image
+import base64
+from io import BytesIO
+import warnings
+warnings.simplefilter('ignore', Image.DecompressionBombWarning)
+from lxml import etree
+from scour.scour import scourString
+
+logger = logging.getLogger(__name__)
+
+DETACHED_PROCESS = 0x00000008
+GROUP_ID = 'export_selection_transform'
+
+class ExportObject(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--tab")
+ pars.add_argument("--wrap_transform", type=inkex.Boolean, default=False, help="Wrap final document in transform")
+ pars.add_argument("--border_offset", type=float, default=1.000, help="Add border offset around selection")
+ pars.add_argument("--border_offset_unit", default="mm", help="Offset unit")
+ pars.add_argument("--export_dir", default="~/inkscape_export/", help="Location to save exported documents")
+ pars.add_argument("--opendir", type=inkex.Boolean, default=False, help="Open containing output directory after export")
+ pars.add_argument("--dxf_exporter_path", default="/usr/share/inkscape/extensions/dxf_outlines.py", help="Location of dxf_outlines.py")
+ pars.add_argument("--export_svg", type=inkex.Boolean, default=False, help="Create a svg file")
+ pars.add_argument("--export_dxf", type=inkex.Boolean, default=False, help="Create a dxf file")
+ pars.add_argument("--export_pdf", type=inkex.Boolean, default=False, help="Create a pdf file")
+ pars.add_argument("--export_png", type=inkex.Boolean, default=False, help="Create a png file")
+ pars.add_argument("--png_dpi", type=float, default=96, help="PNG DPI (applies for export and replace)")
+ pars.add_argument("--replace_by_png", type=inkex.Boolean, default=False, help="Replace selection by png export")
+ pars.add_argument("--newwindow", type=inkex.Boolean, default=False, help="Open file in new Inkscape window")
+ pars.add_argument("--skip_errors", type=inkex.Boolean, default=False, help="Skip on errors")
+
+ def openExplorer(self, dir):
+ if os.name == 'nt':
+ Popen(["explorer", dir], close_fds=True, creationflags=DETACHED_PROCESS).wait()
+ else:
+ Popen(["xdg-open", dir], close_fds=True, start_new_session=True).wait()
+
+ def spawnIndependentInkscape(self, file): #function to spawn non-blocking inkscape instance. the inkscape command is available because it is added to ENVIRONMENT when Inkscape main instance is started
+ if not os.path.exists(file):
+ inkex.utils.debug("Error. {} does not exist!".format(file))
+ exit(1)
+ warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
+ if os.name == 'nt':
+ Popen(["inkscape", file], close_fds=True, creationflags=DETACHED_PROCESS)
+ else:
+ subprocess.Popen(["inkscape", file], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ warnings.simplefilter("default", ResourceWarning)
+
+ def effect(self):
+ 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 \
+ self.options.export_pdf is False and \
+ self.options.export_png is False and \
+ self.options.replace_by_png is False and \
+ self.options.newwindow is False:
+ inkex.utils.debug("You must select at least one option to continue!")
+ return
+
+ if self.options.replace_by_png is True:
+ self.options.border_offset = 0 #override
+
+ if not self.svg.selected:
+ inkex.errormsg("Selection is empty. Please select some objects first!")
+ return
+
+ if self.options.export_dxf is True:
+ #preflight check for DXF input dir
+ if not os.path.exists(self.options.dxf_exporter_path):
+ inkex.utils.debug("Location of dxf_outlines.py does not exist. Please select a proper file and try again.")
+ exit(1)
+
+ export_dir = Path(self.absolute_href(self.options.export_dir))
+ os.makedirs(export_dir, exist_ok=True)
+
+ offset = self.svg.unittouu(str(self.options.border_offset) + self.options.border_offset_unit)
+
+ bbox = inkex.BoundingBox()
+
+ selected = self.svg.selected
+ firstId = selected[0].get('id')
+ parent = self.svg.getElementById(firstId).getparent()
+
+ for element in selected.values():
+ transform = inkex.Transform()
+ parent = element.getparent()
+ if parent is not None and isinstance(parent, inkex.ShapeElement):
+ transform = parent.composed_transform()
+ try:
+ '''
+ ...rectangles cause some strangle scaling issue, offendingly caused by namedview units.
+ The rectangle attributes are set in px. They ignore the real units from namedview.
+ Strange fact: ellipses, spirals and other primitives work flawlessly.
+ '''
+ if isinstance (element, inkex.Rectangle) or \
+ isinstance (element, inkex.Circle) or \
+ isinstance (element, inkex.Ellipse):
+ bbox += element.bounding_box(transform) * scale_factor
+ elif isinstance (element, inkex.TextElement) or \
+ isinstance (element, inkex.Tspan):
+ if self.options.skip_errors is False:
+ self.msg("Text elements are not supported!")
+ return
+ else:
+ continue
+ else:
+ bbox += element.bounding_box(transform)
+ except Exception:
+ logger.exception("Bounding box not computed")
+ logger.info("Skipping bounding box")
+ transform = element.composed_transform()
+ x1, y1 = transform.apply_to_point([0, 0])
+ x2, y2 = transform.apply_to_point([1, 1])
+ bbox += inkex.BoundingBox((x1, x2), (y1, y2))
+
+ template = self.create_document()
+ svg_filename = None
+
+ group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
+ group.attrib['id'] = GROUP_ID
+ group.attrib['transform'] = str(inkex.Transform(((1, 0, -bbox.left), (0, 1, -bbox.top))))
+
+ for element in self.svg.selected.values():
+ if element.tag == inkex.addNS('image', 'svg'):
+ continue #skip images
+ elem_copy = deepcopy(element)
+ elem_copy.attrib['transform'] = str(element.composed_transform())
+ elem_copy.attrib['style'] = str(element.specified_style())
+ group.append(elem_copy)
+
+ template.attrib['viewBox'] = f'{-offset} {-offset} {bbox.width + offset * 2} {bbox.height + offset * 2}'
+ template.attrib['width'] = f'{bbox.width + offset * 2}' + self.svg.unit
+ template.attrib['height'] = f'{bbox.height + offset * 2}' + self.svg.unit
+
+ if svg_filename is None:
+ filename_base = element.attrib.get('id', None).replace(os.sep, '_')
+ if filename_base:
+ svg_filename = filename_base + '.svg'
+ if not filename_base: #should never be the case. Inkscape might crash if the id attribute is empty or not existent due to invalid SVG
+ filename_base = self.svg.get_unique_id("selection")
+ svg_filename = filename_base + '.svg'
+
+ if len(group) == 0:
+ self.msg("Selection does not contain any vector data.")
+ exit(1)
+
+ template.append(group)
+ svg_out = os.path.join(tempfile.gettempdir(), svg_filename)
+
+ if self.options.wrap_transform is False:
+ #self.load(inkscape_command(template.tostring(), select=GROUP_ID, verbs=['SelectionUnGroup;FileSave'])) #fails due to new bug
+
+ #workaround
+ self.save_document(template, svg_out) #export recent file
+ actions_list=[]
+ actions_list.append("SelectionUnGroup")
+ actions_list.append("export-type:svg")
+ 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
+ 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)
+ self.load(svg_out) #reload recent file
+
+ template = self.svg
+ for child in template.getchildren():
+ if child.tag == '{http://www.w3.org/2000/svg}metadata':
+ template.remove(child)
+
+ self.save_document(template, svg_out) # save one into temp dir to access for dxf/pdf/new window instance
+
+ if self.options.export_svg is True:
+ self.save_document(template, export_dir / svg_filename)
+
+ if self.options.opendir is True:
+ self.openExplorer(export_dir)
+
+ if self.options.newwindow is True:
+ #inkscape(os.path.join(export_dir, svg_filename)) #blocking cmd
+ self.spawnIndependentInkscape(os.path.join(tempfile.gettempdir(), svg_filename)) #non-blocking
+
+ if self.options.export_dxf is True:
+ #ensure that python command is available #we pass 25.4/96 which stands for unit mm. See inkex.units.UNITS and dxf_outlines.inx
+ cmd = [
+ sys.executable, #the path of the python interpreter which is used for this script
+ self.options.dxf_exporter_path,
+ '--output=' + os.path.join(export_dir, filename_base + '.dxf'),
+ r'--units=25.4/96',
+ os.path.join(tempfile.gettempdir(), svg_filename)
+ ]
+ proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE)
+ stdout, stderr = proc.communicate()
+ if proc.returncode != 0:
+ 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')))
+ 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)
+
+ if self.options.export_png is True:
+ png_export=os.path.join(export_dir, filename_base + '.png')
+ try:
+ os.remove(png_export)
+ except OSError as e:
+ #inkex.utils.debug("Error while deleting previously generated output file " + png_export)
+ pass
+ actions_list=[]
+ actions_list.append("export-background:white")
+ actions_list.append("export-type:png")
+ actions_list.append("export-dpi:{}".format(self.options.png_dpi))
+ 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)
+ 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)
+
+ if self.options.replace_by_png is True:
+ #export to png file to temp
+ png_export=os.path.join(tempfile.gettempdir(), filename_base + '.png')
+ try:
+ os.remove(png_export)
+ except OSError as e:
+ #inkex.utils.debug("Error while deleting previously generated output file " + png_export)
+ pass
+ actions_list=[]
+ actions_list.append("export-background:white")
+ actions_list.append("export-type:png")
+ actions_list.append("export-dpi:{}".format(self.options.png_dpi))
+ 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)
+ 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)
+ #then remove the selection and replace it by png
+ #self.msg(parent.get('id'))
+ for element in selected.values():
+ element.delete()
+ #read png file and get base64 string from it
+ try:
+ img = Image.open(png_export)
+ except Image.DecompressionBombError as e: #we could also increse PIL.Image.MAX_IMAGE_PIXELS = some large int
+ self.msg("Error. Image is too large ({} x {} px). Reduce DPI and try again!".format(self.svg.uutounit(bbox.width), self.svg.uutounit(bbox.height)))
+ exit(1)
+ output_buffer = BytesIO()
+ img.save(output_buffer, format='PNG')
+ byte_data = output_buffer.getvalue()
+ base64_str = base64.b64encode(byte_data).decode('UTF-8')
+ #finally replace the svg:path(s) with svg:image
+ imgReplacement = etree.SubElement(Rectangle(), '{http://www.w3.org/2000/svg}image')
+ imgReplacement.attrib['x'] = str(bbox.left)
+ imgReplacement.attrib['y'] = str(bbox.top)
+ imgReplacement.attrib['width'] = str(bbox.width)
+ imgReplacement.attrib['height'] = str(bbox.height)
+ imgReplacement.attrib['id'] = firstId
+ imgReplacement.attrib['{http://www.w3.org/1999/xlink}href'] = "data:image/png;base64,{}".format(base64_str)
+ parent.append(imgReplacement)
+ if parent.attrib.has_key('transform'):
+ del parent.attrib['transform'] #remove transform
+
+
+ def create_document(self):
+ document = self.svg.copy()
+ for child in document.getchildren():
+ if child.tag == '{http://www.w3.org/2000/svg}defs':
+ continue
+ document.remove(child)
+ return document
+
+ def save_document(self, document, filename):
+ with open(filename, 'wb') as fp:
+ document = document.tostring()
+ fp.write(scourString(document).encode('utf8'))
+
+
+if __name__ == '__main__':
+ ExportObject().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/export_selection_as/meta.json b/extensions/fablabchemnitz/export_selection_as/meta.json
new file mode 100644
index 0000000..0ca17f8
--- /dev/null
+++ b/extensions/fablabchemnitz/export_selection_as/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Export Selection As ...",
+ "id": "fablabchemnitz.de.export_selection_as",
+ "path": "export_selection_as",
+ "dependent_extensions": null,
+ "original_name": "Export selection as svg",
+ "original_id": "sk.linuxos.export_selection",
+ "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",
+ "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"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.inx b/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.inx
new file mode 100644
index 0000000..a3ed82b
--- /dev/null
+++ b/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.inx
@@ -0,0 +1,56 @@
+
+
+ Guilloche Contour
+ fablabchemnitz.de.guilloche_creations.guilloche_contour
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+ 1
+ 0
+ 0
+ 20
+ false
+
+
+
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.py b/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.py
new file mode 100644
index 0000000..ca994ca
--- /dev/null
+++ b/extensions/fablabchemnitz/guilloche_creations/guilloche_contour.py
@@ -0,0 +1,663 @@
+#! /usr/bin/env python3
+'''
+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.
+
+Quick description:
+
+'''
+# standard library
+from math import *
+from copy import deepcopy
+# local library
+import inkex
+import pathmodifier
+import cubicsuperpath
+from inkex import paths
+from lxml import etree
+
+def getColorAndOpacity(longColor):
+ '''
+ Convert the long into a #rrggbb color value
+ Conversion back is A + B*256^1 + G*256^2 + R*256^3
+ '''
+ longColor = int(longColor)
+
+ if longColor < 0:
+ longColor = longColor & 0xFFFFFFFF
+
+ hexColor = hex(longColor)
+ lhc = len(hexColor)
+
+ hexOpacity = hexColor[lhc-2 : ]
+ hexColor = '#' + hexColor[2:-2].rjust(6, '0')
+
+ return (hexColor, hexOpacity)
+
+def setColorAndOpacity(style, color, opacity):
+ declarations = style.split(';')
+ strokeOpacityInStyle = False
+ newOpacity = round((int(opacity, 16) / 255.0), 8)
+
+ for i,decl in enumerate(declarations):
+ parts = decl.split(':', 2)
+
+ if len(parts) == 2:
+ (prop, val) = parts
+ prop = prop.strip().lower()
+
+ if (prop == 'stroke' and val != color):
+ declarations[i] = prop + ':' + color
+
+ if prop == 'stroke-opacity':
+ if val != newOpacity:
+ declarations[i] = prop + ':' + str(newOpacity)
+
+ strokeOpacityInStyle = True
+
+ if not strokeOpacityInStyle:
+ declarations.append('stroke-opacity' + ':' + str(newOpacity))
+
+ return ";".join(declarations)
+
+def getSkeletonPath(d, offs):
+ '''
+ Recieves a current skeleton path and offset specified by the user if it's line.
+ Calculates new skeleton path to use for creating contour with given offset.
+ '''
+
+ if offs != 0:
+ comps = d.split()
+
+ if ((comps[2] == 'h' or comps[2] == 'H') and len(comps) == 4):
+ startPt = comps[1].split(',')
+ startX = float(startPt[0])
+ startY = float(startPt[1])
+
+ finalX = float(comps[3]) if comps[2] == 'H' else startX + float(comps[3])
+
+ if startX < finalX:
+ startY -= offs
+ else:
+ startY += offs
+
+ comps[1] = startPt[0] + ',' + str(startY)
+ elif ((comps[2] == 'v' or comps[2] == 'V') and len(comps) == 4):
+ startPt = comps[1].split(',')
+ startX = float(startPt[0])
+ startY = float(startPt[1])
+
+ finalY = float(comps[3]) if comps[2] == 'V' else startY + float(comps[3])
+
+ if startY < finalY:
+ startX += offs
+ else:
+ startX -= offs
+
+ comps[1] = str(startX) + ',' + startPt[1]
+ elif (comps[0] == 'M' and len(comps) == 3):
+ startPt = comps[1].split(',')
+ startX = float(startPt[0])
+ startY = float(startPt[1])
+
+ finalPt = comps[2].split(',')
+ finalX = float(finalPt[0])
+ finalY = float(finalPt[1])
+
+ if startX < finalX:
+ if (startY > finalY):
+ startX -= offs
+ finalX -= offs
+ else:
+ startX += offs
+ finalX += offs
+ startY -= offs
+ finalY -= offs
+ else:
+ if startY > finalY:
+ startX -= offs
+ finalX -= offs
+ else:
+ startX += offs
+ finalX += offs
+ startY += offs
+ finalY += offs
+
+ comps[1] = str(startX) + ',' + str(startY)
+ comps[2] = str(finalX) + ',' + str(finalY)
+ elif (comps[0] == 'm' and len(comps) == 3):
+ startPt = comps[1].split(',')
+ startX = float(startPt[0])
+ startY = float(startPt[1])
+
+ finalPt = comps[2].split(',')
+ dx = float(finalPt[0])
+ dy = float(finalPt[1])
+ finalX = startX + dx
+ finalY = startY + dy
+
+ if startX < finalX:
+ if startY > finalY:
+ startX -= offs
+ else:
+ startX += offs
+ startY -= offs
+ else:
+ if startY > finalY:
+ startX -= offs
+ else:
+ startX += offs
+ startY += offs
+
+ comps[1] = str(startX) + ',' + str(startY)
+ comps[2] = str(dx) + ',' + str(dy)
+
+ return paths.CubicSuperPath(paths.Path(' '.join(comps)))
+
+ return paths.CubicSuperPath(paths.Path(d))
+
+def modifySkeletonPath(skelPath):
+
+ resPath = []
+ l = len(skelPath)
+ resPath += skelPath[0]
+
+ if l > 1:
+ for i in range(1, l):
+
+ if skelPath[i][0][1] == resPath[-1][1]:
+ skelPath[i][0][0] = resPath[-1][0]
+ del resPath[-1]
+
+ resPath += skelPath[i]
+
+ return resPath
+
+def linearize(p, tolerance=0.001):
+ '''
+ This function receives a component of a 'cubicsuperpath' and returns two things:
+ The path subdivided in many straight segments, and an array containing the length of each segment.
+ '''
+ zero = 0.000001
+ i = 0
+ d = 0
+ lengths=[]
+
+ while i < len(p) - 1:
+ box = inkex.bezier.pointdistance(p[i][1], p[i][2])
+ box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
+ box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
+ chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
+
+ if (box - chord) > tolerance:
+ b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
+ p[i][2][0], p[i][2][1] = b1[1]
+ p[i + 1][0][0], p[i + 1][0][1] = b2[2]
+ p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
+ else:
+ d = (box + chord) / 2
+ lengths.append(d)
+ i += 1
+
+ new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
+ new.append(p[-1][1])
+ lengths = [l for l in lengths if l > zero]
+
+ return (new, lengths)
+
+def isSkeletonClosed(sklCmp):
+
+ requiredPrecision = 0.005
+
+ sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
+ sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
+
+ if sctest1 or sctest2:
+ return False
+
+ return True
+
+def getPolygonCentroid(polygon):
+ x = 0
+ y = 0
+ n = len(polygon)
+
+ for vert in polygon:
+ x += vert[0]
+ y += vert[1]
+
+ x = x / n
+ y = y / n
+
+ return [x, y]
+
+def getPoint(p1, p2, x, y):
+ x1 = p1[0]
+ y1 = p1[1]
+ x2 = p2[0]
+ y2 = p2[1]
+
+ a = (y1 - y2) / (x1 - x2)
+ b = y1 - a * x1
+
+ if x == None:
+ x = (y - b) / a
+ else:
+ y = a * x + b
+
+ return [x, y]
+
+def getPtOnSeg(p1, p2, segLen, l):
+ if p1[0] == p2[0]:
+ return [p2[0], p2[1] - l] if p2[1] < p1[1] else [p2[0], p2[1] + l]
+
+ if p1[1] == p2[1]:
+ return [p2[0] - l, p2[1]] if p2[0] < p1[0] else [p2[0] + l, p2[1]]
+
+ dy = abs(p1[1] - p2[1])
+ angle = asin(dy / segLen)
+ dx = l * cos(angle)
+ x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
+
+ return getPoint(p1, p2, x, None)
+
+def drawfunction(nodes, width, fx):
+ # x-bounds of the plane
+ xstart = 0.0
+ xend = 2 * pi
+ # y-bounds of the plane
+ ybottom = -1.0
+ ytop = 1.0
+ # size and location of the plane on the canvas
+ height = 2
+ left = 15
+ bottom = 15 + height
+
+ # function specified by the user
+ try:
+ if fx != "":
+ f = eval('lambda x: ' + fx.strip('"'))
+ except SyntaxError:
+ return []
+
+ scalex = width / (xend - xstart)
+ xoff = left
+ # conver x-value to coordinate
+ coordx = lambda x: (x - xstart) * scalex + xoff
+
+ scaley = height / (ytop - ybottom)
+ yoff = bottom
+ # conver y-value to coordinate
+ coordy = lambda y: (ybottom - y) * scaley + yoff
+
+ # step is the distance between nodes on x
+ step = (xend - xstart) / (nodes - 1)
+ third = step / 3.0
+ # step used in calculating derivatives
+ ds = step * 0.001
+
+ # initialize function and derivative for 0;
+ # they are carried over from one iteration to the next, to avoid extra function calculations.
+ x0 = xstart
+ y0 = f(xstart)
+
+ # numerical derivative, using 0.001*step as the small differential
+ x1 = xstart + ds # Second point AFTER first point (Good for first point)
+ y1 = f(x1)
+
+ dx0 = (x1 - x0) / ds
+ dy0 = (y1 - y0) / ds
+
+ # path array
+ a = []
+
+ # Start curve
+ #a.append(['M ', [coordx(x0), coordy(y0)]])
+ a.append(['M', [coordx(x0), coordy(y0)]])
+
+ for i in range(int(nodes - 1)):
+ x1 = (i + 1) * step + xstart
+ x2 = x1 - ds # Second point BEFORE first point (Good for last point)
+ y1 = f(x1)
+ y2 = f(x2)
+
+ # numerical derivative
+ dx1 = (x1 - x2) / ds
+ dy1 = (y1 - y2) / ds
+
+ # create curve
+ a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
+ coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
+ coordx(x1), coordy(y1)]])
+
+ # Next segment's start is this segment's end
+ x0 = x1
+ y0 = y1
+ # Assume the function is smooth everywhere, so carry over the derivative too
+ dx0 = dx1
+ dy0 = dy1
+
+ return a
+
+def offset(pathComp, dx, dy):
+ for ctl in pathComp:
+ for pt in ctl:
+ pt[0] += dx
+ pt[1] += dy
+
+def stretch(pathComp, xscale, yscale, org):
+ for ctl in pathComp:
+ for pt in ctl:
+ pt[0] = org[0] + (pt[0] - org[0]) * xscale
+ pt[1] = org[1] + (pt[1] - org[1]) * yscale
+
+class GuillocheContour(pathmodifier.PathModifier):
+ def add_arguments(self, pars):
+ pars.add_argument("--contourFunction", default="sin", help="Function defining the contour")
+ pars.add_argument("--tab")
+ pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
+ pars.add_argument("--amplitude", type=int, default=1, help="Amplitude of the function")
+ pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
+ pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
+ pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
+ pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If Ttrue, control object will be removed")
+ pars.add_argument("--strokeColor", type=inkex.Color)
+ pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
+ pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
+ pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
+ pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
+ pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
+ pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
+ pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
+ pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
+ pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
+ pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
+
+
+ def prepareSelectionList(self):
+ self.skeletons = self.svg.selected
+ pathmodifier.PathModifier.expand_clones(self, self.skeletons, True, False)
+ pathmodifier.PathModifier.objects_to_paths(self, self.skeletons, True)
+
+ def linearizePath(self, skelPath, offs):
+ comps, lengths = linearize(skelPath)
+
+ self.skelCompIsClosed = isSkeletonClosed(comps)
+
+ if (self.skelCompIsClosed and offs != 0):
+ centroid = getPolygonCentroid(comps)
+
+ for i in range(len(comps)):
+ pt1 = comps[i]
+ dist = inkex.bezier.pointdistance(centroid, pt1)
+
+ comps[i] = getPtOnSeg(centroid, pt1, dist, dist + offs)
+
+ if i > 0:
+ lengths[i - 1] = inkex.bezier.pointdistance(comps[i - 1], comps[i])
+
+ return (comps, lengths)
+
+ def getFunction(self, func):
+ res = ''
+
+ presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
+ presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
+
+ if (func == 'sin' or func == 'cos'):
+ return '(' + str(self.options.amplitude) + ') * ' + func + '(x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + '))'
+
+ if func == 'env1':
+ presetAmp1 = presetAmp3 = 0.495
+ elif func == 'env2':
+ presetAmp1 = presetAmp3 = 0.65
+ presetPhOf1 = presetPhOf3 = 25
+ elif func == 'env3':
+ presetAmp1 = 0.75
+ presetPhOf1 = 25
+ presetAmp3 = 0.24
+ presetPhOf3 = -25
+ elif func == 'env4':
+ presetAmp1 = 1.105
+ presetAmp3 = 0.27625
+ presetPhOf3 = 50
+ elif func == 'env5':
+ presetAmp1 = 0.37464375
+ presetPhOf1 = 25
+ presetAmp2 = 0.5655
+ presetAmp3 = 0.37464375
+ presetPhOf3 = -25
+ elif func == 'env6':
+ presetAmp1 = 0.413725
+ presetPhOf1 = 25
+ presetAmp2 = 0.45695
+ presetPhOf2 = 50
+ presetAmp3 = 0.494
+ presetPhOf3 = -25
+ elif func == 'env7':
+ presetAmp1 = 0.624
+ presetPhOf1 = 25
+ presetAmp2 = 0.312
+ presetAmp3 = 0.624
+ presetPhOf3 = 25
+ elif func == 'env8':
+ presetAmp1 = 0.65
+ presetPhOf1 = 50
+ presetAmp2 = 0.585
+ presetAmp3 = 0.13
+ elif func == 'env9':
+ presetAmp1 = 0.07605
+ presetPhOf1 = 25
+ presetAmp2 = 0.33345
+ presetPhOf2 = 50
+ presetAmp3 = 0.468
+ presetPhOf3 = -25
+ presetAmp4 = 0.32175
+ elif func == 'env10':
+ presetAmp1 = 0.3575
+ presetPhOf1 = -25
+ presetAmp2 = 0.3575
+ presetAmp3 = 0.3575
+ presetPhOf3 = 25
+ presetAmp4 = 0.3575
+ presetPhOf4 = 50
+ elif func == 'env11':
+ presetAmp1 = 0.65
+ presetPhOf1 = 25
+ presetAmp2 = 0.13
+ presetPhOf2 = 50
+ presetAmp3 = 0.26
+ presetPhOf3 = 25
+ presetAmp4 = 0.39
+ elif func == 'env12':
+ presetAmp1 = 0.5525
+ presetPhOf1 = -25
+ presetAmp2 = 0.0414375
+ presetPhOf2 = 50
+ presetAmp3 = 0.15884375
+ presetPhOf3 = 25
+ presetAmp4 = 0.0966875
+ presetAmp5 = 0.28315625
+ presetPhOf5 = -25
+
+ harm1 = '(' + str(self.options.amplitude * (presetAmp1 + self.options.amplitude1)) + ') * cos(1 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
+ harm2 = '(' + str(self.options.amplitude * (presetAmp2 + self.options.amplitude2)) + ') * cos(2 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
+ harm3 = '(' + str(self.options.amplitude * (presetAmp3 + self.options.amplitude3)) + ') * cos(3 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
+ harm4 = '(' + str(self.options.amplitude * (presetAmp4 + self.options.amplitude4)) + ') * cos(4 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
+ harm5 = '(' + str(self.options.amplitude * (presetAmp5 + self.options.amplitude5)) + ') * cos(5 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
+
+ res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
+
+ return res
+
+ def lengthToTime(self, l):
+ '''
+ Recieves an arc length l, and returns the index of the segment in self.skelComp
+ containing the corresponding point, together with the position of the point on this segment.
+
+ If the deformer is closed, do computations modulo the total length.
+ '''
+ if self.skelCompIsClosed:
+ l = l % sum(self.lengths)
+
+ if l <= 0:
+ return 0, l / self.lengths[0]
+
+ i = 0
+
+ while (i < len(self.lengths)) and (self.lengths[i] <= l):
+ l -= self.lengths[i]
+ i += 1
+
+ t = l / self.lengths[min(i, len(self.lengths) - 1)]
+
+ return (i, t)
+
+ def applyDiffeo(self, bpt, vects=()):
+ '''
+ The kernel of this stuff:
+ bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
+ '''
+ s = bpt[0] - self.skelComp[0][0]
+ i, t = self.lengthToTime(s)
+
+ if i == len(self.skelComp) - 1:
+ x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
+ dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
+ dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
+ else:
+ x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
+ dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
+ dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
+
+ vx = 0
+ vy = bpt[1] - self.skelComp[0][1]
+ bpt[0] = x + vx * dx - vy * dy
+ bpt[1] = y + vx * dy + vy * dx
+
+ for v in vects:
+ vx = v[0] - self.skelComp[0][0] - s
+ vy = v[1] - self.skelComp[0][1]
+ v[0] = x + vx * dx - vy * dy
+ v[1] = y + vx * dy + vy * dx
+
+ def effect(self):
+
+ if len(self.options.ids) < 1:
+ inkex.errormsg(_("This extension requires one selected path."))
+ return
+
+ self.prepareSelectionList()
+
+ for skeleton in self.skeletons.__iter__():
+ resPath = []
+ pattern = etree.Element(inkex.addNS('path','svg'))
+
+ self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
+
+ # Copy style of skeleton with setting color and opacity
+ s = skeleton.get('style')
+
+ # Get any path transform for the contour output
+ xfm = skeleton.get('transform')
+
+ firstSkel = skeleton.get('d')
+
+ if s:
+ pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
+
+ if xfm:
+ pattern.set('transform', xfm)
+
+ skeletonPath = modifySkeletonPath(getSkeletonPath(skeleton.get('d'), self.options.offset))
+
+ self.skelComp, self.lengths = self.linearizePath(skeletonPath, self.options.offset)
+
+ length = sum(self.lengths)
+ patternWidth = length / self.options.frequency
+ selectedFunction = self.getFunction(self.options.contourFunction)
+
+ pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
+
+ # Add path into SVG structure
+ skeleton.getparent().append(pattern)
+
+ if self.options.remove:
+ skeleton.getparent().remove(skeleton)
+
+ # Compute bounding box
+ #pattPath = inkex.paths.Path(skeleton.get('d'))
+
+ patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
+ patternPath = inkex.paths.Path(patternCubicPath)
+
+ bbox = patternPath.bounding_box()
+
+ width = bbox.maximum[0] - bbox.minimum[0]
+ dx = width
+
+ if dx < 0.01:
+ exit(_("The total length of the pattern is too small."))
+
+ curPath = deepcopy(patternCubicPath)
+
+ xoffset = self.skelComp[0][0] - bbox.minimum[0]
+ yoffset = self.skelComp[0][1] - (bbox.bottom + bbox.top) / 2
+
+ patternCopies = max(1, int(round(length / dx)))
+ width = dx * patternCopies
+
+ newPath = []
+
+ # Repeat pattern to cover whole skeleton
+ for subPath in curPath:
+ for i in range(0, patternCopies, 1):
+ newPath.append(deepcopy(subPath))
+ offset(subPath, dx, 0)
+
+ curPath = newPath
+
+ # Offset pattern to the first node of the skeleton
+ for subPath in curPath:
+ offset(subPath, xoffset, yoffset)
+
+ # Stretch pattern to whole skeleton
+ for subPath in curPath:
+ stretch(subPath, length / width, 1, self.skelComp[0])
+
+ for subPath in curPath:
+ for ctlpt in subPath:
+ self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
+
+ # Check if there is a need to close path manually
+ if self.skelCompIsClosed:
+ firstPtX = round(curPath[0][0][1][0], 8)
+ firstPtY = round(curPath[0][0][1][1], 8)
+ finalPtX = round(curPath[-1][-1][1][0], 8)
+ finalPtY = round(curPath[-1][-1][1][1], 8)
+
+ if (firstPtX != finalPtX or firstPtY != finalPtY):
+ curPath[-1].append(curPath[0][0])
+
+ resPath += curPath
+
+ # This final step takes the newly constructed contour from a multilevel list to
+ # a formal svg path
+ step1rep = paths.CubicSuperPath(resPath).to_path().to_arrays()
+ step2rep = str(paths.Path(step1rep))
+ pattern.set('d', step2rep)
+
+
+if __name__ == '__main__':
+ GuillocheContour().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.inx b/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.inx
new file mode 100644
index 0000000..1bc959f
--- /dev/null
+++ b/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.inx
@@ -0,0 +1,58 @@
+
+
+ Guilloche Pattern
+ fablabchemnitz.de.guilloche_creations.guilloche_pattern
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+ 100
+ 0
+ 0
+ 100
+ 1
+ 20
+ false
+
+
+
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+ 0.0
+ 0
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.py b/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.py
new file mode 100644
index 0000000..78eb7d1
--- /dev/null
+++ b/extensions/fablabchemnitz/guilloche_creations/guilloche_pattern.py
@@ -0,0 +1,972 @@
+#! /usr/bin/env python3
+'''
+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.
+
+Quick description:
+This extension is an update to the guilloche_pattern extension that works under inkscape v0.92
+
+'''
+# standard library
+from math import asin, atan2, pi, sin, cos
+from copy import deepcopy
+# local library
+import inkex
+import pathmodifier
+import cubicsuperpath
+from inkex import paths
+from lxml import etree
+
+def modifySkeletonPath(skelPath):
+ resPath = []
+ l = len(skelPath)
+ resPath += skelPath[0]
+
+ if l > 1:
+ for i in range(1, l):
+ if skelPath[i][0][1] == resPath[-1][1]:
+ skelPath[i][0][0] = resPath[-1][0]
+ del resPath[-1]
+
+ resPath += skelPath[i]
+
+ return resPath
+
+def linearize(p, tolerance=0.001):
+ '''
+ This function receives a component of a 'cubicsuperpath' and returns two things:
+ The path subdivided in many straight segments, and an array containing the length
+ of each segment.
+ '''
+ zero = 0.000001
+ i = 0
+ d = 0
+ lengths=[]
+
+ while i < len(p) - 1:
+ box = inkex.bezier.pointdistance(p[i][1], p[i][2])
+ box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
+ box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
+ chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
+
+ if (box - chord) > tolerance:
+ b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
+ p[i][2][0], p[i][2][1] = b1[1]
+ p[i + 1][0][0], p[i + 1][0][1] = b2[2]
+ p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
+ else:
+ d = (box + chord) / 2
+ lengths.append(d)
+ i += 1
+
+ new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
+ new.append(p[-1][1])
+ lengths = [l for l in lengths if l > zero]
+
+ return (new, lengths)
+
+def isSkeletonClosed(sklCmp):
+
+ requiredPrecision = 0.005
+
+ sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
+ sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
+
+ if sctest1 or sctest2:
+ return False
+
+ return True
+
+def checkCompatibility(bbox1, bbox2, comps1, comps2):
+ cl1 = isSkeletonClosed(comps1)
+ cl2 = isSkeletonClosed(comps2)
+
+ if (cl1 and cl2):
+ if ((bbox1.left >= bbox2.left) and (bbox1.right <= bbox2.right) and (bbox1.top >= bbox2.top) and (bbox1.bottom <= bbox2.bottom)):
+ return (True, False)
+ elif ((bbox1.left <= bbox2.left) and (bbox1.right >= bbox2.right) and (bbox1.top <= bbox2.top) and (bbox1.bottom >= bbox2.bottom)):
+ return (True, True)
+
+ elif (not cl1 and not cl2):
+ if (comps1[0][0] == comps2[0][0] and comps1[-1][0] == comps2[-1][0]):
+ if ((comps1[0][0] < comps1[-1][0] and comps1[0][1] >= comps2[0][1]) or (comps1[0][0] > comps1[-1][0] and comps1[0][1] <= comps2[0][1])):
+ return (True, False)
+ else:
+ return (True, True)
+
+ elif (comps1[0][1] == comps2[0][1] and comps1[-1][1] == comps2[-1][1]):
+ if ((comps1[0][1] < comps1[-1][1] and comps1[0][0] <= comps2[0][0]) or (comps1[0][1] > comps1[-1][1] and comps1[0][0] >= comps2[0][0])):
+ return (True, False)
+ else:
+ return (True, True)
+
+ return (False, False)
+
+def linearizeEnvelopes(envs):
+
+ env0Path = paths.Path(envs[0].get('d'))
+ env1Path = paths.Path(envs[1].get('d'))
+ env0CubicPath = paths.CubicSuperPath(env0Path)
+ env1CubicPath = paths.CubicSuperPath(env1Path)
+ env0ReformedPath = paths.Path(env0CubicPath)
+ env1ReformedPath = paths.Path(env1CubicPath)
+
+ bbox1 = env0ReformedPath.bounding_box()
+ bbox2 = env1ReformedPath.bounding_box()
+
+ comps1, lengths1 = linearize(modifySkeletonPath(env0CubicPath))
+ comps2, lengths2 = linearize(modifySkeletonPath(env1CubicPath))
+
+ correctness, shouldSwap = checkCompatibility(bbox1, bbox2, comps1, comps2)
+
+ if not shouldSwap:
+ return (comps1, lengths1, comps2, lengths2, bbox1, bbox2, correctness)
+ else:
+ return (comps2, lengths2, comps1, lengths1, bbox2, bbox1, correctness)
+
+def getMidPoint(p1, p2):
+ return [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]
+
+def getPoint(p1, p2, x, y):
+ x1 = p1[0]
+ y1 = p1[1]
+ x2 = p2[0]
+ y2 = p2[1]
+
+ a = (y1 - y2) / (x1 - x2)
+ b = y1 - a * x1
+
+ if x == None:
+ x = (y - b) / a
+ else:
+ y = a * x + b
+
+ return [x, y]
+
+def getPtOnSeg(p1, p2, segLen, l):
+ if p1[0] == p2[0]:
+ return [p1[0], p1[1] - l] if p1[1] > p2[1] else [p1[0], p1[1] + l]
+
+ if p1[1] == p2[1]:
+ return [p1[0] - l, p1[1]] if p1[0] > p2[0] else [p1[0] + l, p1[1]]
+
+ dy = abs(p1[1] - p2[1])
+ angle = asin(dy / segLen)
+ dx = l * cos(angle)
+ x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
+
+ return getPoint(p1, p2, x, None)
+
+def getPtsByX(pt, comps, isClosed):
+ res = []
+
+ for i in range(1, len(comps)):
+ if ((comps[i - 1][0] <= pt[0] and pt[0] <= comps[i][0]) or (comps[i - 1][0] >= pt[0] and pt[0] >= comps[i][0])):
+ if comps[i - 1][0] == comps[i][0]:
+ d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
+ d2 = inkex.bezier.pointdistance(pt, comps[i])
+
+ if d1 < d2:
+ res.append(comps[i - 1])
+ else:
+ res.append(comps[i])
+ elif comps[i - 1][1] == comps[i][1]:
+ res.append([pt[0], comps[i - 1][1]])
+ else:
+ res.append(getPoint(comps[i - 1], comps[i], pt[0], None))
+
+ if not isClosed:
+ return res[0]
+
+ return res
+
+def getPtsByY(pt, comps, isClosed):
+ res = []
+
+ for i in range(1, len(comps)):
+ if ((comps[i - 1][1] <= pt[1] and pt[1] <= comps[i][1]) or (comps[i - 1][1] >= pt[1] and pt[1] >= comps[i][1])):
+ if comps[i - 1][1] == comps[i][1]:
+ d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
+ d2 = inkex.bezier.pointdistance(pt, comps[i])
+
+ if d1 < d2:
+ res.append(comps[i - 1])
+ else:
+ res.append(comps[i])
+ elif comps[i - 1][0] == comps[i][0]:
+ res.append([comps[i - 1][0], pt[1]])
+ else:
+ res.append(getPoint(comps[i - 1], comps[i], None, pt[1]))
+
+ if not isClosed:
+ return res[0]
+
+ return res
+
+def getIntersectionPt(cntr, p, comps):
+ # Find the intersection of the infinitely extended line from cntr to p with comps
+ # This algorithm assumes that each comps segment is a straight line and that the comps segment subtends less than
+ # pi angle at cntr
+
+ twopi = 2.0 * pi
+ a = atan2((cntr[1] - p[1]), (cntr[0] - p[0])) # a is a four-quadrant angle of the cntr->p line, -pi <= a <= pi
+
+ obtPts = []
+
+ for i in range(1, len(comps)): # Check every interval in comps for an intersection
+ a2 = atan2((cntr[1] - comps[i][1]) , (cntr[0] - comps[i][0]))
+ a1 = atan2((cntr[1] - comps[i - 1][1]) , (cntr[0] - comps[i - 1][0]))
+
+ da1a = a - a1 # the angle from a1 to a
+ da12 = a2 - a1 # the angle interval covered by comps[i-1]->comps[i]
+
+ #inkex.errormsg("raw da1a = " + str(da1a))
+ #inkex.errormsg("raw da12 = " + str(da12))
+
+ if da1a > pi: # make the angle fit -pi->pi
+ da1a -= twopi
+ elif da1a < -pi:
+ da1a += twopi
+
+ if da12 > pi: # make the angle fit -pi->pi
+ da12 -= twopi
+ elif da12 < -pi:
+ da12 += twopi
+
+ frac = da1a / da12
+
+ if frac < 0.0 or frac > 1.0: # if the line does not cross the comps interval, move on
+ continue
+
+ x = frac * comps[i][0] + (1.0 - frac) * comps[i-1][0]
+ y = frac * comps[i][1] + (1.0 - frac) * comps[i-1][1]
+
+ obtPts.append([x,y])
+
+ if len(obtPts) < 1:
+ inkex.errormsg("No intersection pt found")
+ exit()
+ else:
+ return obtPts[0]
+
+def getPolygonCentroid(polygon):
+ x = 0
+ y = 0
+ n = len(polygon)
+
+ for vert in polygon:
+ x += vert[0]
+ y += vert[1]
+
+ x = x / n
+ y = y / n
+
+ return [x, y]
+
+def getDistBetweenFirstPts(comps1, comps2, bbox1, bbox2, nest):
+ pt1 = comps1[0]
+ pt2 = None
+
+ if (bbox1[0] == bbox2[0] and bbox1[1] == bbox2[1]):
+ pt2 = getPtsByX(pt1, comps2, False)
+ elif (bbox1[2] == bbox2[2] and bbox1[3] == bbox2[3]):
+ pt2 = getPtsByY(pt1, comps2, False)
+ elif nest:
+ centroid = getPolygonCentroid(comps1)
+ pt2 = getIntersectionPt(centroid, pt1, comps2)
+
+ dist = inkex.bezier.pointdistance(pt1, pt2)
+
+ return dist
+
+def getCirclePath(startPt, rx):
+ curX = startPt[0]
+ curY = startPt[1]
+ signRX = signRY = 1
+
+ res = 'M ' + str(curX) + ',' + str(curY) + ' A '
+
+ for i in range(4):
+ res += str(rx) + ',' + str(rx) + ' 0 0 1 '
+
+ if i % 2 == 0:
+ signRX = -signRX
+ else:
+ signRY = -signRY
+
+ curX += signRX * rx
+ curY += signRY * rx
+
+ res += str(curX) + ',' + str(curY) + ' '
+
+ res += 'Z'
+
+ return res
+
+def getClosedLinearizedSkeletonPath(comps1, comps2, offs, isLine):
+ path = []
+ lengths = []
+
+ centroid = getPolygonCentroid(comps1)
+
+ if isLine:
+ for pt2 in comps2:
+ pt1 = getIntersectionPt(centroid, pt2, comps1)
+ midPt = getMidPoint(pt1, pt2)
+
+ if offs > 0:
+ dist = inkex.bezier.pointdistance(pt1, pt2)
+ path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
+ else:
+ path.append(midPt)
+
+ if len(path) > 1:
+ lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
+ else:
+ pt1 = comps1[0]
+ pt2 = getIntersectionPt(centroid, pt1, comps2)
+ midPt = getMidPoint(pt1, pt2)
+ rx = inkex.bezier.pointdistance(centroid, midPt)
+
+ svgPath = getCirclePath(midPt, rx)
+
+ inkexPath = paths.Path(svgPath)
+ cubicPath = paths.CubicSuperPath(inkexPath)
+ path, lengths = linearize(modifySkeletonPath(cubicPath))
+
+ return (path, lengths)
+
+def getHorizontalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
+ path = []
+ lengths = []
+
+ if isLine:
+ for pt1 in comps1:
+ pt2 = getPtsByX(pt1, comps2, False)
+ midPt = getMidPoint(pt1, pt2)
+
+ if offs > 0:
+ dist = inkex.bezier.pointdistance(pt1, pt2)
+ path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
+ else:
+ path.append(midPt)
+
+ if len(path) > 1:
+ lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
+ else:
+ pt1 = comps1[0]
+ pt2 = comps2[0]
+
+ firstPt = getMidPoint(pt1, pt2)
+ path.append(firstPt)
+
+ lastPt = [comps1[-1][0], firstPt[1]]
+ path.append(lastPt)
+
+ lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
+
+ return (path, lengths)
+
+def getVerticalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
+ path = []
+ lengths = []
+
+ if isLine:
+ for pt1 in comps1:
+ pt2 = getPtsByY(pt1, comps2, False)
+ midPt = getMidPoint(pt1, pt2)
+
+ if offs > 0:
+ dist = inkex.bezier.pointdistance(pt1, pt2)
+ path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
+ else:
+ path.append(midPt)
+
+ if len(path) > 1:
+ lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
+ else:
+ pt1 = comps1[0]
+ pt2 = comps2[0]
+
+ firstPt = getMidPoint(pt1, pt2)
+ path.append(firstPt)
+
+ lastPt = [firstPt[0], comps1[-1][1]]
+ path.append(lastPt)
+
+ lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
+
+ return (path, lengths)
+
+def getColorAndOpacity(longColor):
+ '''
+ Convert the long into a #rrggbb color value
+ Conversion back is A + B*256^1 + G*256^2 + R*256^3
+ '''
+ longColor = int(longColor)
+
+ if longColor < 0:
+ longColor = longColor & 0xFFFFFFFF
+
+ hexColor = hex(longColor)
+ lhc = len(hexColor)
+
+ hexOpacity = hexColor[lhc-2 : ]
+ hexColor = '#' + hexColor[2:-2].rjust(6, '0')
+
+ return (hexColor, hexOpacity)
+
+def setColorAndOpacity(style, color, opacity):
+ declarations = style.split(';')
+ strokeOpacityInStyle = False
+ newOpacity = round((int(opacity, 16) / 255.0), 8)
+
+ for i,decl in enumerate(declarations):
+ parts = decl.split(':', 2)
+
+ if len(parts) == 2:
+ (prop, val) = parts
+ prop = prop.strip().lower()
+
+ if (prop == 'stroke' and val != color):
+ declarations[i] = prop + ':' + color
+
+ if prop == 'stroke-opacity':
+ if val != newOpacity:
+ declarations[i] = prop + ':' + str(newOpacity)
+
+ strokeOpacityInStyle = True
+
+ if not strokeOpacityInStyle:
+ declarations.append('stroke-opacity' + ':' + str(newOpacity))
+
+ return ";".join(declarations)
+
+def drawfunction(nodes, width, fx):
+ # x-bounds of the plane
+ xstart = 0.0
+ xend = 2 * pi
+ # y-bounds of the plane
+ ybottom = -1.0
+ ytop = 1.0
+ # size and location of the plane on the canvas
+ height = 2
+ left = 15
+ bottom = 15 + height
+
+ # function specified by the user
+ try:
+ if fx != "":
+ f = eval('lambda x: ' + fx.strip('"'))
+ except SyntaxError:
+ return []
+
+ scalex = width / (xend - xstart)
+ xoff = left
+ # conver x-value to coordinate
+ coordx = lambda x: (x - xstart) * scalex + xoff
+
+ scaley = height / (ytop - ybottom)
+ yoff = bottom
+ # conver y-value to coordinate
+ coordy = lambda y: (ybottom - y) * scaley + yoff
+
+ # step is the distance between nodes on x
+ step = (xend - xstart) / (nodes - 1)
+ third = step / 3.0
+ # step used in calculating derivatives
+ ds = step * 0.001
+
+ # initialize function and derivative for 0;
+ # they are carried over from one iteration to the next, to avoid extra function calculations.
+ x0 = xstart
+ y0 = f(xstart)
+
+ # numerical derivative, using 0.001*step as the small differential
+ x1 = xstart + ds # Second point AFTER first point (Good for first point)
+ y1 = f(x1)
+
+ dx0 = (x1 - x0) / ds
+ dy0 = (y1 - y0) / ds
+
+ # path array
+ a = []
+ # Start curve
+ a.append(['M', [coordx(x0), coordy(y0)]])
+
+ for i in range(int(nodes - 1)):
+ x1 = (i + 1) * step + xstart
+ x2 = x1 - ds # Second point BEFORE first point (Good for last point)
+ y1 = f(x1)
+ y2 = f(x2)
+
+ # numerical derivative
+ dx1 = (x1 - x2) / ds
+ dy1 = (y1 - y2) / ds
+
+ # create curve
+ a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
+ coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
+ coordx(x1), coordy(y1)]])
+
+ # Next segment's start is this segment's end
+ x0 = x1
+ y0 = y1
+ # Assume the function is smooth everywhere, so carry over the derivative too
+ dx0 = dx1
+ dy0 = dy1
+
+ return a
+
+def offset(pathComp, dx, dy):
+ for ctl in pathComp:
+ for pt in ctl:
+ pt[0] += dx
+ pt[1] += dy
+
+def compsToSVGd(p):
+ f = p[0]
+ p = p[1:]
+ svgd = 'M %.9f,%.9f ' % (f[0], f[1])
+
+ for x in p:
+ svgd += 'L %.9f,%.9f ' % (x[0], x[1])
+
+ return svgd
+
+scRepCounter = 0
+
+def stretchComps(skelComps, patComps, comps1, comps2, bbox1, bbox2, nest, halfHeight, ampl, offs):
+ res = []
+ repCounter = 0
+
+ if nest:
+
+ newPt = None
+ centroid = getPolygonCentroid(comps1)
+
+ for pt in patComps:
+ skelPt = getIntersectionPt(centroid, pt, skelComps)
+ pt1 = getIntersectionPt(centroid, pt, comps1)
+ pt2 = getIntersectionPt(centroid, pt, comps2)
+ midPt = getMidPoint(pt1, pt2)
+
+ dist1 = inkex.bezier.pointdistance(skelPt, pt)
+ dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
+ dist3 = dist2 * dist1 / halfHeight
+
+ if (skelPt[0] >= centroid[0] and skelPt[1] >= centroid[1]):
+ if (pt[0] >= skelPt[0] and pt[1] >= skelPt[1]):
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ elif (skelPt[0] <= centroid[0] and skelPt[1] <= centroid[1]):
+ if (pt[0] <= skelPt[0] and pt[1] <= skelPt[1]):
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ elif (skelPt[0] < centroid[0] and skelPt[1] > centroid[1]):
+ if (pt[0] <= skelPt[0] and pt[1] >= skelPt[1]):
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ else:
+ if (pt[0] >= skelPt[0] and pt[1] <= skelPt[1]):
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+
+ res.append(newPt)
+
+ elif (bbox1.left == bbox2.left and bbox1.right == bbox2.right):
+ midY = skelComps[0][1]
+ newPt = None
+
+ if (patComps[-1][0] != comps1[-1][0] and round(patComps[-1][0], 10) == comps1[-1][0]):
+ patComps[-1][0] = comps1[-1][0]
+
+ for pt in patComps:
+ pt1 = getPtsByX(pt, comps1, False)
+ pt2 = getPtsByX(pt, comps2, False)
+ midPt = getMidPoint(pt1, pt2)
+
+ dist1 = abs(pt[1] - midY)
+ dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
+ dist3 = dist2 * dist1 / halfHeight
+
+ if bbox1.left < bbox1.right:
+ if pt[1] > midY:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ if pt[1] < midY:
+ newPt = getPtOnSeg(midPt, pt1, bezmisc.pointdistance(midPt, pt1), dist3 - offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+
+ res.append(newPt)
+
+ elif (bbox1.top == bbox2.top and bbox1.bottom == bbox2.bottom):
+
+ midX = skelComps[0][0]
+ newPt = None
+
+ if (patComps[-1][1] != comps1[-1][1] and round(patComps[-1][1], 10) == comps1[-1][1]):
+ patComps[-1][1] = comps1[-1][1]
+
+ for pt in patComps:
+ pt1 = getPtsByY(pt, comps1, False)
+ pt2 = getPtsByY(pt, comps2, False)
+ midPt = getMidPoint(pt1, pt2)
+
+ dist1 = abs(pt[0] - midX)
+ dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
+ dist3 = dist2 * dist1 / halfHeight
+
+ if bbox1[2] < bbox1[3]:
+ if pt[0] < midX:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+ else:
+ if pt[1] > midX:
+ newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
+ else:
+ newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
+
+ res.append(newPt)
+
+ return res
+
+def stretch(pathComp, xscale, yscale, org):
+ for ctl in pathComp:
+ for pt in ctl:
+ pt[0] = org[0] + (pt[0] - org[0]) * xscale
+ pt[1] = org[1] + (pt[1] - org[1]) * yscale
+
+class GuillochePattern(pathmodifier.PathModifier):
+ def add_arguments(self, pars):
+ pars.add_argument("--tab")
+ pars.add_argument("--patternFunction", default="sin", help="Function of the pattern")
+ pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
+ pars.add_argument("--amplitude", type=int, default=100, help="Amplitude of the function")
+ pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
+ pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
+ pars.add_argument("--phaseCoverage", type=int, default=100, help="Phase coverage of the function")
+ pars.add_argument("--series", type=int, default=1, help="Series of the function")
+ pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
+ pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If True, control objects will be removed")
+ pars.add_argument("--strokeColor", type=inkex.Color, default=000, help="The line's color")
+ pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
+ pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
+ pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
+ pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
+ pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
+ pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
+ pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
+ pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
+ pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
+ pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
+
+ def prepareSelectionList(self):
+ self.envelopes = self.svg.selected
+ pathmodifier.PathModifier.expand_clones(self, self.envelopes, True, False)
+ pathmodifier.PathModifier.objects_to_paths(self, self.envelopes, True)
+
+ def getFunction(self, func, funcOffs):
+ res = ''
+
+ presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
+ presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
+ funcOffs *= self.options.phaseCoverage / 100.0
+
+ if (func == 'sin' or func == 'cos'):
+ return func + '(x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + '))'
+
+ if func == 'env1':
+ presetAmp1 = presetAmp3 = 0.495
+ elif func == 'env2':
+ presetAmp1 = presetAmp3 = 0.65
+ presetPhOf1 = presetPhOf3 = 25
+ elif func == 'env3':
+ presetAmp1 = 0.75
+ presetPhOf1 = 25
+ presetAmp3 = 0.24
+ presetPhOf3 = -25
+ elif func == 'env4':
+ presetAmp1 = 1.105
+ presetAmp3 = 0.27625
+ presetPhOf3 = 50
+ elif func == 'env5':
+ presetAmp1 = 0.37464375
+ presetPhOf1 = 25
+ presetAmp2 = 0.5655
+ presetAmp3 = 0.37464375
+ presetPhOf3 = -25
+ elif func == 'env6':
+ presetAmp1 = 0.413725
+ presetPhOf1 = 25
+ presetAmp2 = 0.45695
+ presetPhOf2 = 50
+ presetAmp3 = 0.494
+ presetPhOf3 = -25
+ elif func == 'env7':
+ presetAmp1 = 0.624
+ presetPhOf1 = 25
+ presetAmp2 = 0.312
+ presetAmp3 = 0.624
+ presetPhOf3 = 25
+ elif func == 'env8':
+ presetAmp1 = 0.65
+ presetPhOf1 = 50
+ presetAmp2 = 0.585
+ presetAmp3 = 0.13
+ elif func == 'env9':
+ presetAmp1 = 0.07605
+ presetPhOf1 = 25
+ presetAmp2 = 0.33345
+ presetPhOf2 = 50
+ presetAmp3 = 0.468
+ presetPhOf3 = -25
+ presetAmp4 = 0.32175
+ elif func == 'env10':
+ presetAmp1 = 0.3575
+ presetPhOf1 = -25
+ presetAmp2 = 0.3575
+ presetAmp3 = 0.3575
+ presetPhOf3 = 25
+ presetAmp4 = 0.3575
+ presetPhOf4 = 50
+ elif func == 'env11':
+ presetAmp1 = 0.65
+ presetPhOf1 = 25
+ presetAmp2 = 0.13
+ presetPhOf2 = 50
+ presetAmp3 = 0.26
+ presetPhOf3 = 25
+ presetAmp4 = 0.39
+ elif func == 'env12':
+ presetAmp1 = 0.5525
+ presetPhOf1 = -25
+ presetAmp2 = 0.0414375
+ presetPhOf2 = 50
+ presetAmp3 = 0.15884375
+ presetPhOf3 = 25
+ presetAmp4 = 0.0966875
+ presetAmp5 = 0.28315625
+ presetPhOf5 = -25
+
+ harm1 = '(' + str(presetAmp1 + self.options.amplitude1) + ') * cos(1 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
+ harm2 = '(' + str(presetAmp2 + self.options.amplitude2) + ') * cos(2 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
+ harm3 = '(' + str(presetAmp3 + self.options.amplitude3) + ') * cos(3 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
+ harm4 = '(' + str(presetAmp4 + self.options.amplitude4) + ') * cos(4 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
+ harm5 = '(' + str(presetAmp5 + self.options.amplitude5) + ') * cos(5 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
+
+ res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
+
+ return res
+
+ def lengthToTime(self, l):
+ '''
+ Recieves an arc length l, and returns the index of the segment in self.skelComp
+ containing the corresponding point, together with the position of the point on this segment.
+
+ If the deformer is closed, do computations modulo the total length.
+ '''
+ if self.skelCompIsClosed:
+ l = l % sum(self.lengths)
+
+ if l <= 0:
+ return 0, l / self.lengths[0]
+
+ i = 0
+
+ while (i < len(self.lengths)) and (self.lengths[i] <= l):
+ l -= self.lengths[i]
+ i += 1
+
+ t = l / self.lengths[min(i, len(self.lengths) - 1)]
+
+ return (i, t)
+
+ def applyDiffeo(self, bpt, vects=()):
+ '''
+ The kernel of this stuff:
+ bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
+ '''
+ s = bpt[0] - self.skelComp[0][0]
+ i, t = self.lengthToTime(s)
+
+ if i == len(self.skelComp) - 1:
+ x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
+ dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
+ dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
+ else:
+ x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
+ dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
+ dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
+
+ vx = 0
+ vy = bpt[1] - self.skelComp[0][1]
+ bpt[0] = x + vx * dx - vy * dy
+ bpt[1] = y + vx * dy + vy * dx
+
+ for v in vects:
+ vx = v[0] - self.skelComp[0][0] - s
+ vy = v[1] - self.skelComp[0][1]
+ v[0] = x + vx * dx - vy * dy
+ v[1] = y + vx * dy + vy * dx
+
+ def effect(self):
+ if len(self.options.ids) != 2:
+ inkex.errormsg(_("This extension requires two selected paths."))
+ return
+
+ self.prepareSelectionList()
+ envs = list(self.envelopes.values())
+
+ s = envs[0].get('style')
+ parent = envs[0].getparent()
+
+ # Get any path transform for the contour output
+ xfm = envs[0].get('transform')
+
+ fstEnvComps, fstEnvLengths, sndEnvComps, sndEnvLengths, fstEnvBbox, sndEnvBbox, isCorrect = linearizeEnvelopes(envs)
+
+ if not isCorrect:
+ inkex.errormsg(_("Selected paths are not compatible."))
+ return
+
+ areNested = isSkeletonClosed(fstEnvComps) and isSkeletonClosed(sndEnvComps)
+
+ self.skelComp = None
+ self.lengths = None
+
+ countOfSkelPaths = 1
+ distBetweenFirstPts = 1
+ funcSeries = self.options.series
+
+ isLine = True if self.options.patternFunction == 'line' else False
+
+ if (isLine and self.options.offset > 0):
+ distBetweenFirstPts = getDistBetweenFirstPts(fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested)
+ countOfSkelPaths = int(distBetweenFirstPts / self.options.offset)
+ funcSeries = 1
+
+ for cnt in range(0, countOfSkelPaths):
+ curOffset = (cnt + 1) * self.options.offset / distBetweenFirstPts
+
+ if areNested:
+ self.skelComp, self.lengths = getClosedLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
+ elif (fstEnvBbox.left == sndEnvBbox.left and fstEnvBbox.right == sndEnvBbox.right):
+ self.skelComp, self.lengths = getHorizontalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
+ elif (fstEnvBbox.top == sndEnvBbox.top and fstEnvBbox.bottom == sndEnvBbox.bottom):
+ self.skelComp, self.lengths = getVerticalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
+
+ self.skelCompIsClosed = isSkeletonClosed(self.skelComp)
+ length = sum(self.lengths)
+ patternWidth = length / self.options.frequency
+ funcOffsetStep = 100 / funcSeries
+
+ resPath = ''
+
+
+ pattern = etree.Element(inkex.addNS('path','svg'))
+
+ self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
+
+ if s:
+ pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
+
+ if xfm:
+ pattern.set('transform', xfm)
+
+ for j in range(funcSeries):
+ selectedFunction = self.getFunction(self.options.patternFunction, j * funcOffsetStep)
+
+ pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
+
+ # Add path into SVG structure
+ parent.append(pattern)
+
+ patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
+ patternPath = inkex.paths.Path(patternCubicPath)
+
+ # Compute bounding box
+ bbox = patternPath.bounding_box()
+
+ width = bbox.maximum[0] - bbox.minimum[0]
+ height = bbox.maximum[1] - bbox.minimum[1]
+
+ dx = width
+
+ if dx < 0.01:
+ exit(_("The total length of the pattern is too small."))
+
+ curPath = deepcopy(patternCubicPath)
+
+ xoffset = self.skelComp[0][0] - bbox.minimum[0]
+ yoffset = self.skelComp[0][1] - (bbox.maximum[1] + bbox.minimum[1]) / 2
+
+ patternCopies = max(1, int(round(length / dx)))
+ width = dx * patternCopies
+
+ newPath = []
+
+ # Repeat pattern to cover whole skeleton
+ for subPath in curPath:
+ for i in range(0, patternCopies, 1):
+ newPath.append(deepcopy(subPath))
+ offset(subPath, dx, 0)
+
+ # Offset pattern to the first node of the skeleton
+ for subPath in newPath:
+ offset(subPath, xoffset, yoffset)
+
+ curPath = deepcopy(newPath)
+
+ # Stretch pattern to whole skeleton
+ for subPath in curPath:
+ stretch(subPath, length / width, 1, self.skelComp[0])
+
+ for subPath in curPath:
+ for ctlpt in subPath:
+ self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
+
+ # Check if there is a need to close path manually
+ if self.skelCompIsClosed:
+ firstPtX = round(curPath[0][0][1][0], 8)
+ firstPtY = round(curPath[0][0][1][1], 8)
+ finalPtX = round(curPath[-1][-1][1][0], 8)
+ finalPtY = round(curPath[-1][-1][1][1], 8)
+
+ if (firstPtX != finalPtX or firstPtY != finalPtY):
+ curPath[-1].append(curPath[0][0])
+
+ curPathComps, curPathLengths = linearize(modifySkeletonPath(curPath))
+
+ if not isLine:
+ curPathComps = stretchComps(self.skelComp, curPathComps, fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested, height / 2, self.options.amplitude / 100.0, self.options.offset)
+
+ resPath += compsToSVGd(curPathComps)
+
+ pattern.set('d', resPath)
+
+ if self.options.remove:
+ parent.remove(envs[0])
+ parent.remove(envs[1])
+
+if __name__ == '__main__':
+ GuillochePattern().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guilloche_creations/meta.json b/extensions/fablabchemnitz/guilloche_creations/meta.json
new file mode 100644
index 0000000..862ca78
--- /dev/null
+++ b/extensions/fablabchemnitz/guilloche_creations/meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "Guilloche ",
+ "id": "fablabchemnitz.de.guilloche_creations.guilloche_contour",
+ "path": "guilloche_contour",
+ "dependent_extensions": null,
+ "original_name": "Guilloche ",
+ "original_id": "org.inkscape.effect.guilloche_",
+ "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",
+ "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"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/quick_joint/quick_joint.inx b/extensions/fablabchemnitz/quick_joint/quick_joint.inx
index 4ddde8a..c2a7e46 100644
--- a/extensions/fablabchemnitz/quick_joint/quick_joint.inx
+++ b/extensions/fablabchemnitz/quick_joint/quick_joint.inx
@@ -3,14 +3,20 @@
Quick Joint
fablabchemnitz.de.quick_joint
-
+
0
1
+ 1.80
+ 5.50
+ 3.10
+ 10.00
+
1
+ 3.00
3.0
@@ -22,8 +28,10 @@
- false
+ false
+ false
false
+ false
path
diff --git a/extensions/fablabchemnitz/quick_joint/quick_joint.py b/extensions/fablabchemnitz/quick_joint/quick_joint.py
index 9c02aa6..128c2e7 100644
--- a/extensions/fablabchemnitz/quick_joint/quick_joint.py
+++ b/extensions/fablabchemnitz/quick_joint/quick_joint.py
@@ -22,11 +22,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
'''
-import inkex, cmath
-from inkex.paths import Path, ZoneClose, Move
+import inkex, cmath, math
+from inkex import Circle
+from inkex.paths import Path, ZoneClose, Move, Line, line, Curve
from lxml import etree
-debugEn = False
+debugEn = False
def debugMsg(input):
if debugEn:
inkex.utils.debug(input)
@@ -40,21 +41,46 @@ def linesNumber(path):
debugMsg('Number of lines : ' + str(retval))
return retval
-def to_complex(point):
- c = None
- try:
- c = complex(point.x, point.y)
- except Exception as e:
- pass
- if c is not None:
- return c
+class QuickJointPath (Path):
+ def Move(self, point):
+ '''Append an absolute move instruction to the path, to the specified complex point'''
+ debugMsg("- move: " + str(point))
+ self.append(Move(point.real, point.imag))
+
+ def Line(self, point):
+ '''Add an absolute line instruction to the path, to the specified complex point'''
+ debugMsg("- line: " + str(point))
+ self.append(Line(point.real, point.imag))
+
+ def close(self):
+ '''Add a Close Path instriction to the path'''
+ self.append(ZoneClose())
+
+ def line(self, vector):
+ '''Append a relative line command to the path, using the specified vector'''
+ self.append(line(vector.real, vector.imag))
+
+ def get_line(self, n):
+ '''Return the end points of the nth line in the path as complex numbers, as well as whether that line closes the path.'''
+
+ if isinstance(self[n], (Move, Line, ZoneClose)):
+ start = complex(self[n].x, self[n].y)
+ elif isinstance(self[n], Curve):
+ start = complex(self[n].x4, self[n].y4)
+ # If the next point in the path closes the path, go back to the start.
+ end = None
+ closePath = False
+ if isinstance(self[n+1], ZoneClose):
+ end = complex(self[0].x, self[0].y)
+ closePath = True
else:
- inkex.utils.debug("The selection seems not be be a usable polypath. QuickJoint does not operate on curves. Try to flatten bezier curves or splitting up the path.")
- exit(1)
-
+ if isinstance(self[n+1], (Move, Line, ZoneClose)):
+ end = complex(self[n+1].x, self[n+1].y)
+ elif isinstance(self[n+1], Curve):
+ end = complex(self[n+1].x4, self[n+1].y4)
+ return (start, end, closePath)
class QuickJoint(inkex.EffectExtension):
-
def add_arguments(self, pars):
pars.add_argument('-s', '--side', type=int, default=0, help='Object face to tabify')
pars.add_argument('-n', '--numtabs', type=int, default=1, help='Number of tabs to add')
@@ -62,19 +88,17 @@ class QuickJoint(inkex.EffectExtension):
pars.add_argument('-t', '--thickness', type=float, default=3.0, help='Material thickness')
pars.add_argument('-k', '--kerf', type=float, default=0.14, help='Measured kerf of cutter')
pars.add_argument('-u', '--units', default='mm', help='Measurement units')
- pars.add_argument('-e', '--edgefeatures', type=inkex.Boolean, default=False, help='Allow tabs to go right to edges')
pars.add_argument('-f', '--flipside', type=inkex.Boolean, default=False, help='Flip side of lines that tabs are drawn onto')
pars.add_argument('-a', '--activetab', default='', help='Tab or slot menus')
+ pars.add_argument('-S', '--featureStart', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the start edge')
+ pars.add_argument('-E', '--featureEnd', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the end edge')
+ pars.add_argument('-T', '--tSlotEnable', type=inkex.Boolean, default=False, help='Enable to use t-slot definitions')
+ pars.add_argument('-D', '--tSlotHoleDiameter', type=float, default=3.00, help='Diameter of t slot hole')
+ pars.add_argument('-H', '--tSlotNutHeight', type=float, default=1.80, help='Height of t slot nut')
+ pars.add_argument('-W', '--tSlotNutWidth', type=float, default=5.50, help='Width of t slot nut')
+ pars.add_argument('-N', '--tSlotScrewWidth', type=float, default=3.10, help='Scew width of t slot')
+ pars.add_argument('-d', '--tSlotScrewDepth', type=float, default=10.00, help='Screw depth of t slot')
- def to_complex(self, command, line):
- debugMsg('To complex: ' + command + ' ' + str(line))
-
- return complex(line[0], line[1])
-
- def get_length(self, line):
- polR, polPhi = cmath.polar(line)
- return polR
-
def draw_parallel(self, start, guideLine, stepDistance):
polR, polPhi = cmath.polar(guideLine)
polR = stepDistance
@@ -91,192 +115,172 @@ class QuickJoint(inkex.EffectExtension):
debugMsg(polPhi)
debugMsg(cmath.rect(polR, polPhi))
return (cmath.rect(polR, polPhi) + start)
+
- def draw_box(self, start, guideLine, xDistance, yDistance, kerf):
- polR, polPhi = cmath.polar(guideLine)
+ def draw_box(self, start, lengthVector, height, kerf):
+
+ # Kerf is a provided as a positive kerf width. Although tabs
+ # need to be made larger by the width of the kerf, slots need
+ # to be made narrower instead, since the cut widens them.
+
+ # Calculate kerfed height and length vectors
+ heightEdge = self.draw_perpendicular(0, lengthVector, height - kerf, self.flipside)
+ lengthEdge = self.draw_parallel(lengthVector, lengthVector, -kerf)
- #Kerf expansion
- if self.flipside:
- start -= cmath.rect(kerf / 2, polPhi)
- start -= cmath.rect(kerf / 2, polPhi + (cmath.pi / 2))
- else:
- start -= cmath.rect(kerf / 2, polPhi)
- start -= cmath.rect(kerf / 2, polPhi - (cmath.pi / 2))
-
- lines = []
- lines.append(['M', [start.real, start.imag]])
+ debugMsg("draw_box; lengthEdge: " + str(lengthEdge) + ", heightEdge: " + str(heightEdge))
- #Horizontal
- polR = xDistance
- move = cmath.rect(polR + kerf, polPhi) + start
- lines.append(['L', [move.real, move.imag]])
- start = move
+ cursor = self.draw_parallel(start, lengthEdge, kerf/2)
+ cursor = self.draw_parallel(cursor, heightEdge, kerf/2)
- #Vertical
- polR = yDistance
- if self.flipside:
- polPhi += (cmath.pi / 2)
- else:
- polPhi -= (cmath.pi / 2)
- move = cmath.rect(polR + kerf, polPhi) + start
- lines.append(['L', [move.real, move.imag]])
- start = move
+ path = QuickJointPath()
+ path.Move(cursor)
- #Horizontal
- polR = xDistance
- if self.flipside:
- polPhi += (cmath.pi / 2)
- else:
- polPhi -= (cmath.pi / 2)
- move = cmath.rect(polR + kerf, polPhi) + start
- lines.append(['L', [move.real, move.imag]])
- start = move
+ cursor += lengthEdge
+ path.Line(cursor)
- lines.append(['Z', []])
+ cursor += heightEdge
+ path.Line(cursor)
- return lines
-
+ cursor -= lengthEdge
+ path.Line(cursor)
+
+ cursor -= heightEdge
+ path.Line(cursor)
+
+ path.close()
+
+ return path
+
+
def draw_tabs(self, path, line):
- #Male tab creation
- start = to_complex(path[line])
-
- closePath = False
- #Line is between last and first (closed) nodes
- end = None
- if isinstance(path[line+1], ZoneClose):
- end = to_complex(path[0])
- closePath = True
- else:
- end = to_complex(path[line+1])
-
- debugMsg('start')
- debugMsg(start)
- debugMsg('end')
- debugMsg(end)
-
- debugMsg('5-')
-
- if self.edgefeatures:
- segCount = (self.numtabs * 2) - 1
- drawValley = False
- else:
- segCount = (self.numtabs * 2)
- drawValley = False
-
- distance = end - start
- debugMsg('distance ' + str(distance))
- debugMsg('segCount ' + str(segCount))
+ cursor, segCount, segment, closePath = self.get_segments(path, line, self.numtabs)
- try:
- if self.edgefeatures:
- segLength = self.get_length(distance) / segCount
- else:
- segLength = self.get_length(distance) / (segCount + 1)
- except:
- debugMsg('in except')
- segLength = self.get_length(distance)
-
- debugMsg('segLength - ' + str(segLength))
- newLines = []
-
- # when handling firlt line need to set M back
+ # Calculate kerf-compensated vectors for the parallel portion of tab and space
+ tabLine = self.draw_parallel(segment, segment, self.kerf)
+ spaceLine = self.draw_parallel(segment, segment, -self.kerf)
+ endspaceLine = segment
+
+ # Calculate vectors for tabOut and tabIn: perpendicular away and towards baseline
+ tabOut = self.draw_perpendicular(0, segment, self.thickness, not self.flipside)
+ tabIn = self.draw_perpendicular(0, segment, self.thickness, self.flipside)
+
+ debugMsg("draw_tabs; tabLine=" + str(tabLine) + " spaceLine=" + str(spaceLine) + " segment=" + str(segment))
+
+ drawTab = self.featureStart
+ newLines = QuickJointPath()
+
+ # First line is a move or line to our start point
if isinstance(path[line], Move):
- newLines.append(['M', [start.real, start.imag]])
-
- if self.edgefeatures == False:
- newLines.append(['L', [start.real, start.imag]])
- start = self.draw_parallel(start, distance, segLength)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('Initial - ' + str(start))
+ newLines.Move(cursor)
+ else:
+ newLines.Line(cursor)
-
for i in range(segCount):
- if drawValley == True:
- #Vertical
- start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('ValleyV - ' + str(start))
- drawValley = False
- #Horizontal
- start = self.draw_parallel(start, distance, segLength)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('ValleyH - ' + str(start))
+ debugMsg("i = " + str(i))
+ if drawTab == True:
+ debugMsg("- tab")
+ if self.options.tSlotEnable is False:
+ newLines.line(tabOut)
+ newLines.line(tabLine)
+ newLines.line(tabIn)
+ else: #TODO
+ #self.options.tSlotNutHeight
+ #self.options.tSlotNutWidth
+ #self.options.tSlotScrewWidth
+ #self.options.tSlotScrewDepth
+ newLines.line(tabOut)
+ newLines.line(tabLine)
+ newLines.line(tabIn)
else:
- #Vertical
- start = self.draw_perpendicular(start, distance, self.thickness, not self.flipside)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('HillV - ' + str(start))
- drawValley = True
- #Horizontal
- start = self.draw_parallel(start, distance, segLength)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('HillH - ' + str(start))
-
- if self.edgefeatures == True:
- start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
- newLines.append(['L', [start.real, start.imag]])
- debugMsg('Final - ' + str(start))
-
+ if i == 0 or i == segCount - 1:
+ debugMsg("- endspace")
+ newLines.line(endspaceLine)
+ else:
+ debugMsg("- space")
+ newLines.line(spaceLine)
+ drawTab = not drawTab
+
if closePath:
- newLines.append(['Z', []])
+ newLines.close
return newLines
-
+ def add_new_path_from_lines(self, lines, line_style):
+ slot_id = self.svg.get_unique_id('slot')
+ g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
+
+ line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(lines)) }
+ return etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
+
+ def get_segments(self, path, line, num):
+
+ # Calculate number of segments, including all features and spaces
+ segCount = num * 2 - 1
+ if not self.featureStart: segCount = segCount + 1
+ if not self.featureEnd: segCount = segCount + 1
+
+ start, end, closePath = QuickJointPath(path).get_line(line)
+
+ # Calculate the length of each feature prior to kerf compensation.
+ # Here we divide the specified edge into equal portions, one for each feature or space.
+
+ # Because the specified edge has no kerf compensation, the
+ # actual length we end up with will be smaller by a kerf. We
+ # need to use that distance to calculate our segment vector.
+ edge = end - start
+ edge = self.draw_parallel(edge, edge, -self.kerf)
+ segVector = edge / segCount
+
+ debugMsg("get_segments; start=" + str(start) + " end=" + str(end) + " edge=" + str(edge) + " segCount=" + str(segCount) + " segVector=" + str(segVector))
+
+ return (start, segCount, segVector, closePath)
+
def draw_slots(self, path):
- #Female slot creation
+ # Female slot creation
- start = to_complex(path[0])
- end = to_complex(path[1])
+ cursor, segCount, segVector, closePath = self.get_segments(path, 0, self.numslots)
- if self.edgefeatures:
- segCount = (self.numslots * 2) - 1
- else:
- segCount = (self.numslots * 2)
+ # I'm having a really hard time wording why this is necessary, but it is.
+ # get_segments returns a vector based on a narrower edge; adjust that edge to fit within the edge we were given.
+ cursor = self.draw_parallel(cursor, segVector, self.kerf/2)
- distance = end - start
- debugMsg('distance ' + str(distance))
- debugMsg('segCount ' + str(segCount))
-
- try:
- if self.edgefeatures:
- segLength = self.get_length(distance) / segCount
- else:
- segLength = self.get_length(distance) / (segCount + 1)
- except:
- segLength = self.get_length(distance)
-
- debugMsg('segLength - ' + str(segLength))
newLines = []
-
line_style = str(inkex.Style({ 'stroke': '#000000', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('0.1mm')) }))
-
+ drawSlot = self.featureStart
+
for i in range(segCount):
- if (self.edgefeatures and (i % 2) == 0) or (not self.edgefeatures and (i % 2)):
- newLines = self.draw_box(start, distance, segLength, self.thickness, self.kerf)
- debugMsg(newLines)
+ if drawSlot:
+ slot = self.add_new_path_from_lines(self.draw_box(cursor, segVector, self.thickness, self.kerf), line_style)
+ if self.options.tSlotEnable is True:
+ cx, cy = slot.bounding_box().center
+ circle = slot.getparent().add(inkex.Circle(id=self.svg.get_unique_id('tSlotHole')))
+ circle.set('transform', "rotate({:0.6f} {:0.6f} {:0.6f})".format(0, cx, cy))
+ circle.set('r', "{:0.6f}".format(self.tSlotHoleDiameter / 2 - self.kerf))
+ circle.set('cx', "{:0.6f}".format(cx))
+ circle.set('cy', "{:0.6f}".format(cy))
+ circle.style = line_style
- slot_id = self.svg.get_unique_id('slot')
- g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
-
- line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(newLines)) }
- etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
-
- #Find next point
- polR, polPhi = cmath.polar(distance)
- polR = segLength
- start = cmath.rect(polR, polPhi) + start
-
+ cursor = cursor + segVector
+ drawSlot = not drawSlot
+ debugMsg("i: " + str(i) + ", cursor: " + str(cursor))
+ # (We don't modify the path so we don't need to close it)
+
def effect(self):
self.side = self.options.side
self.numtabs = self.options.numtabs
self.numslots = self.options.numslots
self.thickness = self.svg.unittouu(str(self.options.thickness) + self.options.units)
+ self.tSlotNutHeight = self.svg.unittouu(str(self.options.tSlotNutHeight) + self.options.units)
+ self.tSlotNutWidth = self.svg.unittouu(str(self.options.tSlotNutWidth) + self.options.units)
+ self.tSlotScrewWidth = self.svg.unittouu(str(self.options.tSlotScrewWidth) + self.options.units)
+ self.tSlotScrewDepth = self.svg.unittouu(str(self.options.tSlotScrewDepth) + self.options.units)
+ self.tSlotHoleDiameter = self.svg.unittouu(str(self.options.tSlotHoleDiameter) + self.options.units)
self.kerf = self.svg.unittouu(str(self.options.kerf) + self.options.units)
self.units = self.options.units
- self.edgefeatures = self.options.edgefeatures
+ self.featureStart = self.options.featureStart
+ self.featureEnd = self.options.featureEnd
self.flipside = self.options.flipside
self.activetab = self.options.activetab
-
+
for id, node in self.svg.selected.items():
debugMsg(node)
debugMsg('1')
@@ -298,13 +302,13 @@ class QuickJoint(inkex.EffectExtension):
debugMsg(newPath)
debugMsg('4')
debugMsg( p[lineNum + 1:])
- finalPath = p[:lineNum] + newPath + p[lineNum + 1:]
-
+ finalPath = p[:lineNum + 1] + newPath + p[lineNum + 2:]
+
debugMsg(finalPath)
node.set('d',str(Path(finalPath)))
elif self.activetab == 'slotpage':
newPath = self.draw_slots(p)
-
+
if __name__ == '__main__':
QuickJoint().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/visicut/meta.json b/extensions/fablabchemnitz/visicut/meta.json
new file mode 100644
index 0000000..f1b1134
--- /dev/null
+++ b/extensions/fablabchemnitz/visicut/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "",
+ "id": "fablabchemnitz.de.open_in_visicut_",
+ "path": "visicut",
+ "dependent_extensions": null,
+ "original_name": "",
+ "original_id": "visicut.",
+ "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",
+ "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"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/visicut/open_in_visicut.py b/extensions/fablabchemnitz/visicut/open_in_visicut.py
new file mode 100644
index 0000000..1e7f369
--- /dev/null
+++ b/extensions/fablabchemnitz/visicut/open_in_visicut.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+'''
+This extension strips everything which is not selected from
+the current svg, saves it and
+calls VisiCut on it.
+
+Copyright (C) 2012 Thomas Oster, thomas.oster@rwth-aachen.de
+Copyright (C) 2014-2022 Max Gaukler, development@maxgaukler.de
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+
+import sys
+import os
+import re
+import subprocess
+from subprocess import Popen
+import traceback
+import tempfile
+import unicodedata
+import codecs
+import random
+import string
+import socket
+import inkex
+
+try:
+ from os import fsencode
+except ImportError:
+ fsencode = lambda x: x.encode(sys.getfilesystemencoding())
+
+def get_single_instance_port():
+ """
+ get the single instance port used by VisiCut.
+ CAUTION: This code must behave exactly the same as ApplicationInstanceManager.getSingleInstancePort() in Visicut.
+ """
+ port = 6543
+ if (sys.platform == "linux"):
+ d = os.environ.get("DISPLAY")
+ if (d != None):
+ d = d.split(':')[1].split('.')[0]
+ port += int(d)
+
+ if (sys.platform == "win32"):
+ d = os.environ.get("SESSIONNAME")
+ if d == None:
+ # no Terminal Services installed
+ pass
+ else:
+ # get session ID
+ try:
+ CREATE_NO_WINDOW = 0x08000000
+ id = subprocess.check_output("powershell -Command (get-process -pid $pid).sessionid",creationflags=CREATE_NO_WINDOW)
+ id = int(id.strip())
+ port += 2 + id
+ except:
+ inkex.utils.debug("Warning: Cannot determine session ID. please report this.\n")
+ return port
+
+# if Visicut or Inkscape cannot be found, change these lines here to VISICUTDIR="C:/Programs/Visicut" or wherever you installed it.
+# please use forward slashes (/), not backslashes (\).
+#
+# example:
+# VISICUTDIR="C:/Program Files (x86)/VisiCut/"
+# INKSCAPEDIR="C:/Program Files (x86)/Inkscape/"
+VISICUTDIR = ""
+INKSCAPEDIR = ""
+
+# whether to add (true) or replace (false) current visicut's content
+IMPORT = True
+# Store the IDs of selected Elements
+elements = []
+
+arguments=[]
+
+for arg in sys.argv[1:]:
+ if arg[0] == "-":
+ if len(arg) >= 5 and arg[0:5] == "--id=":
+ elements += [arg[5:]]
+ elif len(arg) >= 13 and arg[0:13] == "--visicutbin=":
+ # unused
+ pass
+ # VISICUTBIN=arg[13:]
+ elif len(arg) >= 9 and arg[0:9] == "--import=":
+ IMPORT = "true" in arg[9:]
+ else:
+ arguments += [arg]
+ else:
+ filename = arg
+
+if IMPORT:
+ # do not replace old
+ arguments += ["--add"]
+
+# find executable in the PATH
+def which(program, extraPaths=[]):
+ pathlist = extraPaths + os.environ["PATH"].split(os.pathsep) + [""]
+ if "nt" in os.name: # Windows
+ if not program.lower().endswith(".exe"):
+ program += ".exe"
+ programfiles = os.environ.get("ProgramFiles", "C:\\Program Files\\")
+ programfiles86 = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)\\")
+ # also look in %ProgramFiles%/yourProgram/yourProgram.exe
+ pathlist += [programfiles + "\\" + program + "\\", programfiles86 + "\\" + program + "\\"]
+
+ def is_exe(fpath):
+ return os.path.isfile(fpath) and (os.access(fpath, os.X_OK) or fpath.endswith(".exe"))
+ for path in pathlist:
+ exe_file = os.path.join(path, program)
+ if is_exe(exe_file):
+ return exe_file
+ raise Exception("Cannot find executable {0} in PATH={1}.\n\n"
+ "Please report this bug on https://github.com/t-oster/VisiCut/issues\n\n"
+ "For a quick fix: Set VISICUTDIR and INKSCAPEDIR in "
+ "{2}"
+ .format(repr(program), repr(pathlist), os.path.realpath(__file__)))
+
+
+def inkscape_version():
+ """Return Inkscape version number as float, e.g. version "0.92.4" --> return: float 0.92"""
+ version = subprocess.check_output([INKSCAPEBIN, "--version"], stderr=subprocess.DEVNULL).decode('ASCII', 'ignore')
+ assert version.startswith("Inkscape ")
+ match = re.match("Inkscape ([0-9]+\.[0-9]+).*", version)
+ assert match is not None
+ version_float = float(match.group(1))
+ return version_float
+
+
+
+# Strip SVG to only contain selected elements, convert objects to paths, unlink clones
+# Inkscape version: takes care of special cases where the selected objects depend on non-selected ones.
+# Examples are linked clones, flowtext limited to a shape and linked flowtext boxes (overflow into the next box).
+#
+# Inkscape is called with certain "verbs" (gui actions) to do the required cleanup
+# The idea is similar to http://bazaar.launchpad.net/~nikitakit/inkscape/svg2sif/view/head:/share/extensions/synfig_prepare.py#L181 , but more primitive - there is no need for more complicated preprocessing here
+def stripSVG_inkscape(src, dest, elements):
+ version = inkscape_version()
+
+ # create temporary file for opening with inkscape.
+ # delete this file later so that it will disappear from the "recently opened" list.
+ tmpfile = tempfile.NamedTemporaryFile(delete=False, prefix='temp-visicut-', suffix='.svg')
+ tmpfile.close()
+ tmpfile = tmpfile.name
+ import shutil
+ shutil.copyfile(src, tmpfile)
+
+
+ if version < 1:
+ # inkscape 0.92 long-term-support release. Will be in Linux distributions until 2025 or so
+ # Selection commands: select items, invert selection, delete
+ selection = []
+ for el in elements:
+ selection += ["--select=" + el]
+
+ if len(elements) > 0:
+ # selection += ["--verb=FitCanvasToSelection"] # TODO add a user configuration option whether to keep the page size (and by this the position relative to the page)
+ selection += ["--verb=EditInvertInAllLayers", "--verb=EditDelete"]
+
+
+ hidegui = ["--without-gui"]
+
+ # currently this only works with gui because of a bug in inkscape: https://bugs.launchpad.net/inkscape/+bug/843260
+ hidegui = []
+
+ command = [INKSCAPEBIN] + hidegui + [tmpfile, "--verb=UnlockAllInAllLayers", "--verb=UnhideAllInAllLayers"] + selection + ["--verb=EditSelectAllInAllLayers", "--verb=EditUnlinkClone", "--verb=ObjectToPath", "--verb=FileSave", "--verb=FileQuit"]
+ elif version < 1.2:
+ # Inkscape 1.0 (released ca 2020) or 1.1
+ # inkscape --select=... --verbs=...
+ # (see inkscape --help, inkscape --verb-list)
+ command = [INKSCAPEBIN, tmpfile, "--batch-process"]
+ verbs = ["ObjectToPath", "UnlockAllInAllLayers"]
+ if elements: # something is selected
+ # --select=object1,object2,object3,...
+ command += ["--select=" + ",".join(elements)]
+ else:
+ verbs += ["EditSelectAllInAllLayers"]
+ verbs += ["UnhideAllInAllLayers", "EditInvertInAllLayers", "EditDelete", "EditSelectAllInAllLayers", "EditUnlinkClone", "ObjectToPath", "FileSave"]
+ # --verb=action1;action2;...
+ command += ["--verb=" + ";".join(verbs)]
+
+
+ DEBUG = False
+ if DEBUG:
+ # Inkscape sometimes silently ignores wrong verbs, so we need to double-check that everything's right
+ for verb in verbs:
+ verb_list = [line.split(":")[0] for line in subprocess.check_output([INKSCAPEBIN, "--verb-list"], stderr=subprocess.DEVNULL).split("\n")]
+ if verb not in verb_list:
+ inkex.utils.debug("Inkscape does not have the verb '{}'. Please report this as a VisiCut bug.".format(verb))
+ else:
+ # Inkscape 1.2 (released 2022)
+ # inkscape --export-overwrite --actions=action1;action2...
+ # (see inkscape --help, inkscape --action-list)
+ # (for debugging, you can look at the intermediate state by running inkscape --with-gui --actions=... my_filename.svg)
+ # Note that it is (almost?) impossible to find a sequence that works in all cases.
+ # Cases to consider:
+ # - selecting whole groups
+ # - selecting objects within a group
+ # - selecting across groups/layers (e.g., enter group, select something, then Shift-click to select things from other layers)
+ # Difficulties with Inkscape:
+ # - "invert selection" does not behave as expected in all these cases,
+ # for example if a group is selected then inverting can select the elements within.
+ # - Inkscape has a wonderful --export-id commandline switch, but it only works correctly with one ID
+
+ # Solution:
+ actions = ["unlock-all"]
+ if elements:
+ # something was selected when calling the plugin.
+ # -> Recreate that selection
+ # - select objects
+ actions += ["select-by-id:" + ",".join(elements)]
+ else:
+ # - select all
+ actions += ["select-all:all"]
+ # - convert to path
+ actions += ["clone-unlink", "object-to-path"]
+ if elements:
+ # ensure that only the selection is exported:
+ # - create group of selection
+ actions += ["selection-group"]
+ # - set group ID to a known value. Use a pseudo-random value to avoid collisions
+ target_group_id = "TARGET-GROUP-" + "".join(random.sample(string.ascii_lowercase, 20))
+ actions += ["object-set-attribute:id," + target_group_id]
+ # - set export options: use only the target group ID, nothing else
+ actions += ["export-id-only:true", "export-id:" + target_group_id]
+ # - do export (keep position on page)
+ actions += ["export-area-page"]
+
+ command = [INKSCAPEBIN, tmpfile, "--export-overwrite", "--actions=" + ";".join(actions)]
+
+ try:
+ #sys.stderr.write(" ".join(command))
+ # run inkscape, buffer output
+ subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True)
+ except subprocess.CalledProcessError as e:
+ inkex.utils.debug("Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect.\nInkscape's output was:\n" + e.output)
+
+ # move output to the intended destination filename
+ os.rename(tmpfile, dest)
+
+
+"""
+Get document name (original filename) from Inkscape SVG
+
+Inkscape saves the file to a random temporary name.
+However, the original filename is stored inside the SVG.
+"""
+
+
+def get_original_filename(filename):
+ docname = None
+
+ # parse SVG for docname tag
+ with codecs.open(filename, "r", encoding='utf-8') as f:
+ for line in f:
+ if 'sodipodi:docname="' in line:
+ matches = re.search('sodipodi:docname="(.*).svg"', line)
+ if not matches:
+ break
+ try:
+ docname = matches.group(1)
+ except IndexError:
+ # something is wrong with this line
+ break
+ # unescape XML string
+
+ docname = docname.replace('<', '<')
+ docname = docname.replace('>', '>')
+ docname = docname.replace('"', '"')
+ docname = docname.replace('&', '&')
+
+ # normalize accented characters (äöü -> aou)
+ docname = unicodedata.normalize('NFKD', docname).encode('ASCII', 'ignore').decode('ASCII')
+ break
+
+ if not docname:
+ # failed to read filename from SVG, return original one
+ docname = os.path.basename(filename)
+ if str.endswith(docname, ".svg"):
+ docname = docname[:-4]
+ if str.startswith(docname, "ink_ext_"):
+ # inkscape temporary file, the name is useless
+ docname = "new"
+
+ # sanitize the filename:
+ # filter out special characters (@/\& ...)
+ docname = "".join(x for x in docname if (x.isalnum() or x in "._- "))
+ docname = docname + ".svg"
+ return docname
+
+# find executable paths
+import platform
+if platform.system() == 'Darwin':
+ VISICUTBIN = which("VisiCut.MacOS", [VISICUTDIR])
+elif "nt" in os.name: # Windows
+ VISICUTBIN = which("VisiCut.exe", [VISICUTDIR])
+else:
+ VISICUTBIN = which("VisiCut.Linux", [VISICUTDIR, "/usr/share/visicut"])
+INKSCAPEBIN = which("inkscape", [INKSCAPEDIR])
+
+tmpdir = tempfile.mkdtemp(prefix='temp-visicut-')
+dest_filename = os.path.join(tmpdir, get_original_filename(filename))
+
+# remove all non-selected elements and convert inkscape-specific elements (text-to-path etc.)
+stripSVG_inkscape(src=filename, dest=dest_filename, elements=elements)
+
+# Try to connect to running VisiCut instance
+# Note: this step may be omitted, as VisiCut will do the same.
+# However, doing it here saves 2-3 seconds of waiting time on Windows.
+s = socket.socket()
+try:
+ s.connect(("localhost", get_single_instance_port()))
+except socket.error:
+ pass
+else:
+ if IMPORT:
+ s.send(b"@" + fsencode(dest_filename) + b"\n")
+ else:
+ s.send(fsencode(dest_filename) + b"\n")
+ sys.exit(0)
+finally:
+ s.close()
+
+# Try to start own VisiCut instance
+try:
+ creationflags = 0
+ close_fds = False
+ if os.name == "nt":
+ DETACHED_PROCESS = 8 # start as "daemon"
+ creationflags = DETACHED_PROCESS
+ close_fds = True
+ else:
+ try:
+ import daemonize
+ daemonize.createDaemon()
+ except:
+ inkex.utils.debug("Could not daemonize. Sorry, but Inkscape was blocked until VisiCut is closed")
+ cmd = [VISICUTBIN] + arguments + [dest_filename]
+ with subprocess.Popen(cmd, creationflags=creationflags, close_fds=close_fds, stderr=subprocess.DEVNULL) as p:
+ p.communicate()
+except:
+ inkex.utils.debug("Can not start VisiCut (" + str(sys.exc_info()[0]) + "). Please start manually or change the VISICUTDIR variable in the Inkscape-Extension script\n")
+
+
+
+# TODO (complicated, probably WONTFIX): cleanup temporary directories -- this is really difficult because we need to make sure that visicut no longer needs the file, even for reloading!
diff --git a/extensions/fablabchemnitz/visicut/open_in_visicut_add.inx b/extensions/fablabchemnitz/visicut/open_in_visicut_add.inx
new file mode 100644
index 0000000..4800ea7
--- /dev/null
+++ b/extensions/fablabchemnitz/visicut/open_in_visicut_add.inx
@@ -0,0 +1,17 @@
+
+
+ Open In VisiCut (Add)
+ fablabchemnitz.de.open_in_visicut_add
+ true
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/visicut/open_in_visicut_replace.inx b/extensions/fablabchemnitz/visicut/open_in_visicut_replace.inx
new file mode 100644
index 0000000..afa2783
--- /dev/null
+++ b/extensions/fablabchemnitz/visicut/open_in_visicut_replace.inx
@@ -0,0 +1,17 @@
+
+
+ Open In VisiCut (Replace)
+ fablabchemnitz.de.open_in_visicut_replace
+ false
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file