diff --git a/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.inx b/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.inx
new file mode 100644
index 0000000..be70cbc
--- /dev/null
+++ b/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.inx
@@ -0,0 +1,25 @@
+
+
+ FreeCAD Sketch Export
+ fablabchemnitz.de.svg2fcsketch
+
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.py b/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.py
new file mode 100644
index 0000000..587fe4c
--- /dev/null
+++ b/extensions/fablabchemnitz/svg2fcsketch/svg2fcsketch.py
@@ -0,0 +1,1919 @@
+#! /usr/bin/python3
+'''
+(C) 2018 juergen@fabmail.org
+Distribute under GPL-2.0 or ask.
+
+References:
+ https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Sketcher/TestSketcherApp.py
+ /usr/lib/freecad-daily/Mod/Sketcher/SketcherExample.py
+ https://en.wikipedia.org/wiki/Rytz%27s_construction#Computer_aided_solution
+ http://wiki.inkscape.org/wiki/index.php/Python_modules_for_extensions
+ https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve
+ https://en.wikipedia.org/wiki/B-spline#Relationship_to_piecewise/composite_B%C3%A9zier
+
+v0.1 jw, initial draft refactoring inksvg to make it fit here.
+v0.2 jw, Introducing class SketchPathGen to seperate the sketch generator from the svg parser.
+v0.3 jw, correct _coord_from_svg() size and offset handling. Suppress
+ silly version printing, that would ruin an inkscape extension.
+V0.4 jw, Added GuiDocument.xml for visibility and camera defaults.
+ Using BoundBox() to compute camera placement.
+V0.5 jw, objEllipse() done correctly with _ellipse_vertices2d()
+V0.6 jw, objArc() done. ArcOfCircle() is a strange beast with rotation and mirroring.
+V0.7 jw, pathString() done.
+V0.8 jw, imported class SubPathTracker() from src/examples/sketch_spline.py
+
+-----------------------
+
+inksvg.py - parse an svg file into a plain list of paths.
+
+(C) 2017 juergen@fabmail.org, authors of eggbot and others.
+
+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
+
+#################
+2017-12-04 jw, v1.0 Refactored class InkSvg from cookiecutter extension
+2017-12-07 jw, v1.1 Added roundedRectBezier()
+2017-12-10 jw, v1.3 Added styleDasharray() with stroke-dashoffset
+2017-12-14 jw, v1.4 Added matchStrokeColor()
+2017-12-21 jw, v1.5 Changed getPathVertices() to construct a to self.paths list, instead of
+ a dictionary. (Preserving native ordering)
+2017-12-22 jw, v1.6 fixed "use" to avoid errors with unknown global symbal 'composeTransform'
+2017-12-25 jw, v1.7 Added getNodeStyle(), cssDictAdd(), expanded matchStrokeColor() to use
+ inline style defs. Added a warning message for not-implemented CSS styles.
+ v1.7a Added getNodeStyleOne() made getNodeStyle() recurse through parents.
+2018-03-10 jw, v1.7b Added search paths to find inkex.
+ v1.7c Refactoring for simpler interface without subclassing.
+ Added load(), getElementsByIds() methods.
+2018-03-21 jw, v1.7d Added handleViewBox() to load().
+ Added traverse().
+'''
+
+from optparse import OptionParser
+import os
+import sys
+import math
+sys_platform = sys.platform.lower()
+if sys_platform.startswith('win'):
+ sys.path.append('C:\Program Files\Inkscape\share\extensions')
+elif sys_platform.startswith('darwin'):
+ sys.path.append('~/.config/inkscape/extensions')
+else: # Linux
+ sys.path.append('/usr/share/inkscape/extensions/')
+sys.path.append('/usr/lib64/freecad-daily/lib64/') # prefer daily over normal.
+sys.path.append('/usr/lib64/freecad/lib64/')
+
+verbose=-1 # -1=quiet, 0=normal, 1=babble
+epsilon = 0.00001
+if verbose <= 0:
+ os.dup2(1,99) # hack to avoid silly version string printing.
+ f = open("/dev/null", "w")
+ os.dup2(f.fileno(), 1)
+
+# The version printing code has
+# src/App/Application.cpp: if (!(mConfig["Verbose"] == "Strict"))
+# but we cannot call SetConfig('Verbose', 'Strict') early enough.
+from FreeCAD import Base, BoundBox
+sys.stdout.flush() # push silly version string into /dev/null
+if verbose <= 0:
+ f.close()
+ os.dup2(99,1) # back in cansas.
+
+import Part, Sketcher # causes SEGV if Base is not yet imported from FreeCAD
+import ProfileLib.RegularPolygon as Poly
+import gettext
+import re
+import inkex
+from inkex import Transform, CubicSuperPath
+import cspsubdiv
+import bezmisc
+from lxml import etree
+
+class PathGenerator():
+ """
+ A PathGenerator has methods for different svg objects. It compiles an
+ internal representation of them all, handling transformations and linear
+ interpolation of curved path segments.
+
+ The base class PathGenerator is dummy (abstract) class that raises an
+ NotImplementedError() on each method entry point. It serves as documentation for
+ the generator interface.
+ """
+ def __init__(self):
+ self._svg = None
+
+ def registerSvg(self, svg):
+ self._svg = svg
+ svg.stats = self.stats
+
+ def pathString(self, d, node, mat):
+ """
+ d is expected formatted as an svg path string here.
+ """
+ raise NotImplementedError("See example inksvg.LinearPathGen.pathString()")
+
+ def pathList(self, d, node, mat):
+ """
+ d is expected as an [[cmd, [args]], ...] arrray
+ """
+ raise NotImplementedError("See example inksvg.LinearPathGen.pathList()")
+
+ def objRect(x, y, w, h, node, mat):
+ raise NotImplementedError("See example inksvg.LinearPathGen.objRect()")
+
+ def objRoundedRect(self, x, y, w, h, rx, ry, node, mat):
+ raise NotImplementedError("See example inksvg.LinearPathGen.objRoundedRect()")
+
+ def objEllipse(self, cx, xy, rx, ry, node, mat):
+ raise NotImplementedError("See example inksvg.LinearPathGen.objEllipse()")
+
+ def objArc(self, d, cx, cy, rx, ry, st, en, cl, node, mat):
+ """
+ SVG does not have an arc element. Inkscape creates officially a path element,
+ but also (redundantly) provides the original arc values.
+ Implementations can choose to work with the path d and ignore the rest,
+ or work with the cx, cy, rx, ry, ... parameters and ignore d.
+ Note: the parameter closed=True/False is actually derived from looking at the last
+ command of path d. Hackish, but there is no 'sodipodi:closed' element, or similar.
+ """
+ raise NotImplementedError("See example inksvg.LinearPathGen.objArc()")
+
+class LinearPathGen(PathGenerator):
+
+ def __init__(self, smoothness=0.2):
+ self.smoothness = max(0.0001, smoothness)
+
+ def pathString(self, d, node, mat):
+ """
+ d is expected formatted as an svg path string here.
+ """
+ inkex.utils.debug("calling getPathVertices", self.smoothness)
+ self._svg.getPathVertices(d, node, mat, self.smoothness)
+
+ def pathList(self, d, node, mat):
+ """
+ d is expected as an [[cmd, [args]], ...] arrray
+ """
+ return self.pathString(str(d.path), node, mat)
+
+ def objRect(x, y, w, h, node, mat):
+ """
+ Manually transform
+
+
+
+ into
+
+
+
+ I.e., explicitly draw three sides of the rectangle and the
+ fourth side implicitly
+ """
+ a = []
+ a.append(['M ', [x, y]])
+ a.append([' l ', [w, 0]])
+ a.append([' l ', [0, h]])
+ a.append([' l ', [-w, 0]])
+ a.append([' Z', []])
+ self.pathList(a, node, matNew)
+
+ def objRoundedRect(self, x, y, w, h, rx, ry, node, mat):
+ inkex.utils.debug("calling roundedRectBezier")
+ d = self._svg.roundedRectBezier(x, y, w, h, rx, ry)
+ self._svg.getPathVertices(d, node, mat, self.smoothness)
+
+ def objEllipse(self, cx, xy, rx, ry, node, mat):
+ """
+ Convert circles and ellipses to a path with two 180 degree
+ arcs. In general (an ellipse), we convert
+
+
+
+ to
+
+
+
+ where
+
+ X1 = CX - RX
+ X2 = CX + RX
+
+ Note: ellipses or circles with a radius attribute of value 0
+ are ignored
+ """
+ x1 = cx - rx
+ x2 = cx + rx
+ d = 'M %f,%f ' % (x1, cy) + \
+ 'A %f,%f ' % (rx, ry) + \
+ '0 1 0 %f,%f ' % (x2, cy) + \
+ 'A %f,%f ' % (rx, ry) + \
+ '0 1 0 %f,%f' % (x1, cy)
+ self.pathString(d, node, matNew)
+
+ def objArc(self, d, cx, cy, rx, ry, st, en, cl, node, mat):
+ """
+ We ignore the cx, cy, rx, ry data, and are happy that inkscape
+ also provides the same information as a path.
+ """
+ self.pathString(d, node, matNew)
+class InkSvg():
+ """
+ Usage example with subclassing:
+
+ #
+ # class ThunderLaser(inkex.Effect):
+ # def __init__(self):
+ # inkex.localize()
+ # inkex.Effect.__init__(self)
+ # def effect(self):
+ # svg = InkSvg(document=self.document, pathgen=LinearPathGen(smoothness=0.2))
+ # svg.handleViewBox()
+ # svg.recursivelyTraverseSvg(self.document.getroot(), svg.docTransform)
+ # for tup in svg.paths:
+ # node = tup[0]
+ # ...
+ # e = ThunderLaser()
+ # e.affect()
+ #
+
+ Simple usage example with method invocation:
+
+ # svg = InkSvg(pathgen=LinearPathGen(smoothness=0.01))
+ # svg.load(svgfile)
+ # svg.traverse([ids...])
+ # inkex.utils.debug(svg.pathgen.path)
+
+ """
+ __version__ = "1.7c"
+ DEFAULT_WIDTH = 100
+ DEFAULT_HEIGHT = 100
+
+ # imports from inkex
+ NSS = inkex.NSS
+
+ def getElementsByIds(self, ids):
+ """
+ ids be a string of a comma seperated values, or a list of strings.
+ Returns a list of xml nodes.
+ """
+ if not self.document:
+ raise ValueError("no document loaded.")
+ if isinstance(ids, (bytes, str)): ids = [ ids ] # handle some scalars
+ ids = ','.join(ids).split(',') # merge into a string and re-split
+
+ ## OO-Fail:
+ # cannot use inkex.getElementById() -- it returns only the first element of each hit.
+ # cannot use inkex.getselected() -- it returns the last element of each hit only.
+ """Collect selected nodes"""
+ nodes = []
+ for id in ids:
+ if id != '': # empty strings happen after splitting...
+ path = '//*[@id="%s"]' % id
+ el_list = self.document.xpath(path, namespaces=InkSvg.NSS)
+ if el_list:
+ for node in el_list:
+ nodes.append(node)
+ else:
+ raise ValueError("id "+id+" not found in the svg document.")
+ return nodes
+
+
+ def load(self, filename):
+ inkex.localization.localize()
+ # OO-Fail: cannot call inkex.Effect.parse(), Effect constructor has so many side-effects.
+ stream = open(filename, 'r')
+ p = etree.XMLParser(huge_tree=True)
+ self.document = etree.parse(stream, parser=p)
+ stream.close()
+ # initialize a coordinate system that can be picked up by pathgen.
+ self.handleViewBox()
+
+ def traverse(self, ids=None):
+ """
+ Recursively traverse the SVG document. If ids are given, all matching nodes
+ are taken as start positions for traversal. Otherwise traveral starts at
+ the root node of the document.
+ """
+ selected = []
+ if ids is not None:
+ selected = self.getElementsByIds(ids)
+ if len(selected):
+ # Traverse the selected objects
+ for node in selected:
+ transform = self.recursivelyGetEnclosingTransform(node)
+ self.recursivelyTraverseSvg([node], transform)
+ else:
+ # Traverse the entire document building new, transformed paths
+ self.recursivelyTraverseSvg(self.document.getroot(), self.docTransform)
+
+
+ def getNodeStyleOne(self, node):
+ """
+ Finds style declarations by .class, #id or by tag.class syntax,
+ and of course by a direct style='...' attribute.
+ """
+ sheet = ''
+ selectors = []
+ classes = node.get('class', '') # classes == None can happen here.
+ if classes is not None and classes != '':
+ selectors = ["."+cls for cls in re.split('[\s,]+', classes)]
+ selectors += [node.tag+sel for sel in selectors]
+ node_id = node.get('id', '')
+ if node_id is not None and node_id != '':
+ selectors += [ "#"+node_id ]
+ for sel in selectors:
+ if sel in self.css_dict:
+ sheet += '; '+self.css_dict[sel]
+ style = node.get('style', '')
+ if style is not None and style != '':
+ sheet += '; '+style
+ return dict(inkex.Style.parse_str(sheet))
+
+ def getNodeStyle(self, node):
+ combined_style = {}
+ parent = node.getparent()
+ if parent.tag == inkex.addNS('g','svg') or parent.tag == 'g':
+ combined_style = self.getNodeStyle(parent)
+ style = self.getNodeStyleOne(node)
+ for s in style:
+ combined_style[s] = style[s] # overwrite or add
+ return combined_style
+
+
+ def styleDasharray(self, path_d, node):
+ """
+ Check the style of node for a stroke-dasharray, and apply it to the
+ path d returning the result. d is returned unchanged, if no
+ stroke-dasharray was found.
+
+ ## Extracted from inkscape extension convert2dashes; original
+ ## comments below.
+ ## Added stroke-dashoffset handling, made it a universal operator
+ ## on nodes and 'd' paths.
+
+ This extension converts a path into a dashed line using 'stroke-dasharray'
+ It is a modification of the file addnodes.py
+
+ Copyright (C) 2005,2007 Aaron Spike, aaron@ekips.org
+ Copyright (C) 2009 Alvin Penner, penner@vaxxine.com
+ """
+
+ def tpoint(p1, p2, t = 0.5):
+ x1 = p1[0]
+ y1 = p1[1]
+ x2 = p2[0]
+ y2 = p2[1]
+ return [x1+t*(x2-x1),y1+t*(y2-y1)]
+ def cspbezsplit(sp1, sp2, t = 0.5):
+ m1=tpoint(sp1[1],sp1[2],t)
+ m2=tpoint(sp1[2],sp2[0],t)
+ m3=tpoint(sp2[0],sp2[1],t)
+ m4=tpoint(m1,m2,t)
+ m5=tpoint(m2,m3,t)
+ m=tpoint(m4,m5,t)
+ return [[sp1[0][:],sp1[1][:],m1], [m4,m,m5], [m3,sp2[1][:],sp2[2][:]]]
+ def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.001):
+ bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
+ t = bezmisc.beziertatlength(bez, l, tolerance)
+ return cspbezsplit(sp1, sp2, t)
+ def cspseglength(sp1,sp2, tolerance = 0.001):
+ bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
+ return bezmisc.bezierlength(bez, tolerance)
+
+ style = self.getNodeStyle(node)
+ if not style.has_key('stroke-dasharray'):
+ return path_d
+ dashes = []
+ if style['stroke-dasharray'].find(',') > 0:
+ dashes = [float (dash) for dash in style['stroke-dasharray'].split(',') if dash]
+ if not dashes:
+ return path_d
+
+ dashoffset = 0.0
+ if style.has_key('stroke-dashoffset'):
+ dashoffset = float(style['stroke-dashoffset'])
+ if dashoffset < 0.0: dashoffset = 0.0
+ if dashoffset > dashes[0]: dashoffset = dashes[0] # avoids a busy-loop below!
+
+ p = CubicSuperPath(d)
+ new = []
+ for sub in p:
+ idash = 0
+ dash = dashes[0]
+ # inkex.utils.debug("initial dash length: ", dash, dashoffset)
+ dash = dash - dashoffset
+ length = 0
+ new.append([sub[0][:]])
+ i = 1
+ while i < len(sub):
+ dash = dash - length
+ length = cspseglength(new[-1][-1], sub[i])
+ while dash < length:
+ new[-1][-1], next, sub[i] = cspbezsplitatlength(new[-1][-1], sub[i], dash/length)
+ if idash % 2: # create a gap
+ new.append([next[:]])
+ else: # splice the curve
+ new[-1].append(next[:])
+ length = length - dash
+ idash = (idash + 1) % len(dashes)
+ dash = dashes[idash]
+ if idash % 2:
+ new.append([sub[i]])
+ else:
+ new[-1].append(sub[i])
+ i+=1
+ return CubicSuperPath(new).to_path(curves_only=True)
+
+ def matchStrokeColor(self, node, rgb, eps=None, avg=True):
+ """
+ Return True if the line color found in the style attribute of elem
+ does not differ from rgb in any of the components more than eps.
+ The default eps with avg=True is 64.
+ With avg=False the default is eps=85 (33% on a 0..255 scale).
+
+ In avg mode, the average of all three color channel differences is
+ compared against eps. Otherwise each color channel difference is
+ compared individually.
+
+ The special cases None, False, True for rgb are interpreted logically.
+ Otherwise rgb is expected as a list of three integers in 0..255 range.
+ Missing style attribute or no stroke element is interpreted as False.
+ Unparseable stroke elements are interpreted as 'black' (0,0,0).
+ Hexadecimal stroke formats of '#RRGGBB' or '#RGB' are understood
+ as well as 'rgb(100%, 0%, 0%) or 'red'.
+ """
+ if eps is None:
+ eps = 64 if avg == True else 85
+ if rgb is None or rgb is False: return False
+ if rgb is True: return True
+ style = self.getNodeStyle(node)
+ s = style.get('stroke', '')
+ if s == '': return False
+ c = inkex.Color(s).to_rgb()
+ if sum:
+ s = abs(rgb[0]-c[0]) + abs(rgb[1]-c[1]) + abs(rgb[2]-c[2])
+ if s < 3*eps:
+ return True
+ return False
+ if abs(rgb[0]-c[0]) > eps: return False
+ if abs(rgb[1]-c[1]) > eps: return False
+ if abs(rgb[2]-c[2]) > eps: return False
+ return True
+
+ def cssDictAdd(self, text):
+ """
+ Represent css cdata as a hash in css_dict.
+ Implements what is seen on: http://www.blooberry.com/indexdot/css/examples/cssembedded.htm
+ """
+ text=re.sub('^\s*(
+
+
+
+
+
+
+
+
+
+ %s
+
+""" % ('Sketch_'+docname, camera_xml)
+
+try:
+ import zipfile
+ z = zipfile.ZipFile(fcstdfile, 'a')
+ z.writestr('GuiDocument.xml', guidoc_xml)
+ z.close()
+except:
+ inkex.utils.debug(guidoc_xml)
+ inkex.utils.debug("Warning: Failed to add GuiDocument.xml to %s -- camera and visibility are undefined." % fcstdfile)
+
+if verbose > -1:
+ inkex.utils.debug("%s written." % fcstdfile)
+
+if not options.outfile:
+ out = open(fcstdfile,'rb')
+ sys.stdout.buffer.write(out.read())
+ out.close()
+ os.unlink(fcstdfile)
\ No newline at end of file