diff --git a/extensions/fablabchemnitz/parallel_translation/parallel_translation.inx b/extensions/fablabchemnitz/parallel_translation/parallel_translation.inx
new file mode 100644
index 00000000..fd89cc13
--- /dev/null
+++ b/extensions/fablabchemnitz/parallel_translation/parallel_translation.inx
@@ -0,0 +1,99 @@
+
+
+ Parallel Translation
+ fablabchemnitz.de.parallel_translation
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+ false
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 15
+ 0x20f020ff
+ false
+ false
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/parallel_translation/parallel_translation.py b/extensions/fablabchemnitz/parallel_translation/parallel_translation.py
new file mode 100644
index 00000000..cccd1d78
--- /dev/null
+++ b/extensions/fablabchemnitz/parallel_translation/parallel_translation.py
@@ -0,0 +1,620 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 Christian Vogt
+#
+# recursiveFuseTransform() has originally been written by
+# Mark "Klowner" Riedesel
+# see: https://github.com/Klowner/inkscape-applytransforms
+#
+# edgeResize() has been inspired by inkscape-round-corners extension by
+# Juergen Weigert
+# see: https://github.com/jnweiger/inkscape-round-corners
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2, as
+# published by the Free Software Foundation.
+#
+# 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.
+#
+
+
+# This extension was written and tested using Inkscape V1.0.2
+# Most probably, it will not work on versions prior to V1.0
+#
+# Resources I found being useful and inspiring:
+# https://gitlab.com/inkscape/extensions/-/wikis/My-First-Effect-Extension
+# https://wiki.inkscape.org/wiki/index.php?title=Extension_subsystem
+# https://wiki.inkscape.org/wiki/index.php?title=Extensions:_INX_widgets_and_parameters
+# https://inkscape.gitlab.io/extensions/documentation/inkex.html
+# https://inkscape-extensions-guide.readthedocs.io/en/latest/inkex-modules.html#
+# https://gitlab.com/inkscape/extensions
+# https://inkscape.gitlab.io/inkscape/doxygen-extensions/index.html
+
+
+# The purpose of this extension is to help performing parallel
+# translations of selected straight paths (lines). This is equivalent
+# to changing the absolute X coordinate of a vertical line or changing
+# the absolute Y coordinate of a horizontal line, but for lines drawn
+# in any angle.
+#
+# The second purpose is to align a group of objects to such a line
+# drawn in any angle. This means: moving and rotating the group and
+# adjust its width to match the line length.
+#
+# To align two objects, it is also possible to turn one of them into
+# a group by adding a 'rotation center object' in the middle of the
+# selected path/lines, and rotate the group into its 0-degrees position.
+# This group can then aligned to the other object.
+#
+# V0.1 2021-04-20 : initial version.
+#
+# V0.2 2021-04-21 :
+# - Fix: avoid using deprecated inkex API.
+# - Fix: move both point and handles of a node to avoid
+# non-intentional creating curves out of flat line segments.
+# - New: added 'endpoint tolerance' parameter (--endpTol)
+# - New: now supports having the rotation center object outside the
+# horizontal center of the group.
+#
+# V0.3 2021-04-28 :
+# - New: now allows to select two nodes of a larger path to be used
+# as straight line to use for calculating the length and
+# translation angle.
+# - Mod: Added 'global' section to the information printout.
+# - Mod: For closed paths (start point matches end point), we
+# use a hardcoded translation angle of 0 degrees.
+#
+# V0.4 2021-04-29 :
+# - New: Added the 'Obj-to-Group' tab.
+# - Mod: Updated the info-tab description.
+#
+# V0.5 2021-05-04 :
+# - New: Added 'remove rotation center object' checkbox.
+# - Mod: Added LICENCE file.
+# - Mod: Changed default rotation center object color to neon green.
+# (because pink has already been used for cut-lines in some
+# existing drawings)
+#
+# V0.6 2021-05-05 :
+# - Mod: added a workaround for un-selecting a path after we've
+# created a rotation-group out of it.
+#
+# V0.7 2021-05-11 :
+# - Mod: Added more error-checking to the 'Obj-to-Group' function.
+# - Mod: 'Obj-to-Group' is now able to group multiple objects.
+# - Mod: Changed copyright and put version information into inx-file.
+#
+# V1.0 2021-09-03 :
+# - Doc: Moved to github repository.
+# - Doc: Added README.md and screenshot art.
+# - Doc: Now using the mail address that corresponds to github account.
+# - Fix: Fixed stretch/resize option of group alignment.
+
+
+import math
+import inkex
+from inkex.paths import CubicSuperPath, Path
+from inkex.transforms import Transform
+from inkex.styles import Style
+
+NULL_TRANSFORM = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
+
+
+class ParallelTranlation(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--tab")
+ # 'translation' Tab
+ pars.add_argument("--copyMode" , type=str , default="none", help="Move copy or original")
+ pars.add_argument("--distance" , type=float , default=0 , help="Distance to move")
+ pars.add_argument("--distUnit" , type=str , default="mm" , help="Unit of the distance")
+ pars.add_argument("--reverse" , type=inkex.Boolean, default=False , help="Move in opposite direction")
+ pars.add_argument("--useFixedAngle", type=inkex.Boolean, default=False , help="use fixed translation angle")
+ # 'align' Tab
+ pars.add_argument("--fixedAngle" , type=float , default=0 , help="Translation angle")
+ pars.add_argument("--copyModeA" , type=str , default="none", help="Align copy or original")
+ pars.add_argument("--lengthModeA" , type=str , default="none", help="Group length adjustment method")
+ pars.add_argument("--endpTol" , type=int , default=15 , help="Enpoint tolerance (percent of group width)")
+ pars.add_argument("--colorA" , type=int , default=0 , help="Color of rotation center circle")
+ pars.add_argument("--rmFromGroup" , type=inkex.Boolean, default=False , help="remove rotation center object from aligned group")
+ pars.add_argument("--reverseA" , type=inkex.Boolean, default=False , help="additinal group rotate by 180 degrees")
+ # 'group' Tab
+ pars.add_argument("--ctSize" , type=float , default=1 , help="Size of rotation center object")
+ pars.add_argument("--ctSzUnit" , type=str , default="mm" , help="Unit of the size")
+ pars.add_argument("--reverseG" , type=inkex.Boolean, default=False , help="additinal group rotate by 180 degrees")
+
+
+ def effect(self):
+ pathCount = 0
+ pathsWNodesCount = 0
+ groupCount = 0
+ for elem in self.svg.selected.values():
+ if isinstance(elem, inkex.PathElement):
+ pathCount += 1
+ nodes = self.count_selected_nodes( elem.get_id() )
+ if nodes > 0:
+ if nodes == 2:
+ pathsWNodesCount += 1
+ self.groupCenterPath = elem
+ else:
+ raise inkex.AbortExtension(
+ "When selecting individual nodes, you should "
+ "always select exactly two from the same "
+ "(sub-)path.")
+
+ elif isinstance(elem, inkex.Group):
+ groupCount += 1
+ self.alignGroup = elem
+
+ if self.options.tab == "align":
+ if pathCount == 0 or groupCount == 0:
+ raise inkex.AbortExtension(
+ "In alignment mode, please select exactly one group "
+ "and at least one path to align it to.")
+ if groupCount > 1:
+ raise inkex.AbortExtension(
+ "Sorry, we can align a single group only.")
+ if pathCount > 1 and self.options.copyModeA != 'copy':
+ raise inkex.AbortExtension(
+ "To align to multiple paths at once, please choose "
+ "to apply the alignment to copies of the group.")
+ elif self.options.tab == "group":
+ if pathsWNodesCount != 1:
+ raise inkex.AbortExtension(
+ "In group mode, please select exactly two nodes "
+ "from the same (sub-)path.")
+ else:
+ if pathCount == 0:
+ raise inkex.AbortExtension(
+ "Please select one path at least.")
+
+ if self.options.tab == "info":
+ msg = "Global info:"
+ self.msg(msg)
+ msg = " Document name: {}"
+ self.msg(msg.format(self.svg.name))
+ msg = " Dimensions: width={} height={} scale={} unit={}"
+ self.msg(msg.format(self.svg.width, self.svg.height, self.svg.scale, self.svg.unit))
+ msg = " Selected: {} group(s), {} path(s), {} node(s)"
+ self.msg(msg.format(groupCount, pathCount, len(self.options.selected_nodes)))
+ if self.options.selected_nodes:
+ msg = " Nodes: {}"
+ self.msg(msg.format(self.options.selected_nodes))
+ self.msg(' ')
+
+ if self.options.tab == "group":
+ self.process_node(self.groupCenterPath)
+ else:
+ for elem in self.svg.selected.values():
+ self.process_node(elem)
+
+
+ @staticmethod
+ def close_enough(a, b, maxdist):
+ # two numbers are compared for close-enough numerical equality
+ if maxdist <= 0 :
+ eps = 1e-9
+ else:
+ eps = maxdist
+ return abs(a-b) <= eps
+
+
+ def edgeResize(self, obj, length, rbb):
+ bbox = obj.bounding_box()
+ dx = (length - bbox.width) / 2
+ offcenter = rbb.x.center - bbox.x.center
+ maxdist = (bbox.width * self.options.endpTol) / 100
+
+ # Iterate over all children of the group-object.
+ # Find path nodes located at the left and right edges of the
+ # groups bounding box, and move these by 'dx' further to the
+ # left or right.
+ for child in obj.iterchildren():
+
+ if not isinstance(child, inkex.PathElement):
+ continue #ignore all non-path-objects
+
+ csp = child.path.to_superpath()
+ node_count = 0;
+ for sub in csp:
+ for node in sub:
+ node_x = node[1][0]
+ if self.close_enough( node_x, bbox.left, maxdist ):
+ for i in range(0, 3):
+ node[i][0] -= (dx - offcenter)
+ node_count += 1
+ """msg = "id:{} touched left edge at:{}"
+ self.msg(msg.format(child.get_id(),
+ bbox.left))"""
+
+ if self.close_enough( node_x, bbox.right, maxdist ):
+ for i in range(0, 3):
+ node[i][0] += (dx + offcenter)
+ node_count += 1
+ """msg = "id:{} touched right edge at:{}"
+ self.msg(msg.format(child.get_id(),
+ bbox.right))"""
+
+ if node_count > 0:
+ # we've moved some nodes of this superpath!
+ # convert back to real path and modify child object in-place
+ child.set_path(csp.to_path(curves_only=False))
+
+
+ def align(self, x, y, alpha, length):
+ rotation_col = inkex.Color(self.options.colorA).to_rgba()
+ rotation_bb = None
+
+ # select the object to move
+ if self.options.copyModeA in ('copy'):
+ objToMove = self.alignGroup.duplicate()
+ else:
+ if self.options.copyModeA in ('obj'):
+ self.alignGroup.duplicate()
+ objToMove = self.alignGroup
+
+ """
+ msg = "align goup {}"
+ self.msg(msg.format(self.alignGroup.get_id()))
+ msg = " to: x={} y={}"
+ self.msg(msg.format(x,y))
+ msg = " length: {}"
+ self.msg(msg.format(length))
+ msg = " angle : {}"
+ self.msg(msg.format(math.degrees(alpha)))
+ msg = " rotation center color:{}"
+ self.msg(msg.format(rotation_col))
+ for child in self.alignGroup.iterchildren():
+ bbox = child.bounding_box()
+ msg = " child-obj:{} fill:{} stroke:{} x={} y={}"
+ self.msg(msg.format(child.get_id(),
+ child.style.get_color(name='fill'),
+ child.style.get_color(name='stroke'),
+ bbox.x.center,
+ bbox.y.center))
+ """
+ # first, apply any transformations the object may have already
+ # so we are starting with no transform assigned to the group
+ self.recursiveFuseTransform(objToMove)
+
+ # locate the rotation center marker in the object to move by
+ # checking for the rotation marker fill color within the group
+ for child in objToMove.iterchildren():
+ if child.style.get_color(name='fill') == rotation_col:
+ rotation_bb = child.bounding_box()
+ break
+ if rotation_bb is None:
+ raise inkex.AbortExtension(
+ "No rotation center object found in group.")
+
+ # adjust the objects length. Since we haven't moved or rotated
+ # it by now, we have to adjust its width only.
+ if self.options.lengthModeA != "none":
+ if self.options.lengthModeA == "scale":
+ if not math.isclose( rotation_bb.x.center, objToMove.bounding_box().x.center, rel_tol=1e-05):
+ msg = "Rotation center x = {}"
+ self.msg(msg.format(rotation_bb.x.center))
+ msg = "Group center x = {}"
+ self.msg(msg.format(objToMove.bounding_box().x.center))
+ raise inkex.AbortExtension(
+ "Warning: rotation center is outside the groups horizontal center. "
+ "This is not supported in stretch/resize adjustment mode. "
+ "You may want to try the line endpoint adjust mode intead.")
+
+ tr = inkex.Transform()
+ tr.add_scale( length / objToMove.bounding_box().width, 1 )
+ objToMove.transform = tr * objToMove.transform
+ self.recursiveFuseTransform(objToMove)
+ # it looks like the scale transformation also applies
+ # some horizontal movement, so we need to retrieve
+ # the rotation_bb again afterwards.
+ rotation_bb = child.bounding_box()
+
+ elif self.options.lengthModeA == "endpoints":
+ self.edgeResize( objToMove, length, rotation_bb )
+
+ # Remove the rotation center object from the group if we have
+ # been instructed to do so.
+ if self.options.rmFromGroup:
+ child.delete()
+
+ # then, move it to the desired location
+ dx = x - rotation_bb.x.center
+ dy = y - rotation_bb.y.center
+ tr = inkex.Transform()
+ tr.add_translate( dx, dy )
+ objToMove.transform = tr * objToMove.transform
+ self.recursiveFuseTransform(objToMove)
+
+ # finally, rotate it by the given angle at the desired location
+ tr = inkex.Transform()
+ tr.add_rotate( math.degrees(alpha), x, y )
+ objToMove.transform = tr * objToMove.transform
+ self.recursiveFuseTransform(objToMove)
+
+
+ def make_group(self, parent, x, y, alpha):
+ # Add a new group and put a rotation center object into it.
+ group = parent.add(inkex.Group())
+ style = "color:#000000;fill:{};stroke-width:1;stroke:none".format(
+ str(inkex.Color(self.options.colorA).to_rgb()))
+ size = self.svg.unittouu('{}{}'.format(self.options.ctSize,
+ self.options.ctSzUnit) )
+ circle = group.add(inkex.Circle(cx=str(x), cy=str(y), r=str(size/2)))
+ circle.style = inkex.Style().parse_str(style)
+
+ # put duplicates of all selected elements into the group.
+ for elem in self.svg.selected.values():
+ group.add(elem.duplicate())
+
+ # Rotate the whole new group back to it's zero position
+ tr = inkex.Transform()
+ tr.add_rotate( math.degrees(-alpha), x, y )
+ group.transform = tr * group.transform
+ self.recursiveFuseTransform(group)
+
+ # delete all selected elements to remove the selection.
+ # Note that self.svg.set_selected() don't work. See:
+ # https://inkscape.org/forums/extensions/how-to-clear-node-selection/
+ for elem in self.svg.selected.values():
+ elem.delete()
+
+
+
+ # Returns the number of selected nodes from the object given
+ # by name. Indices of selected nodes matching the given object name
+ # and subpath index are appended to the given index list
+ def count_selected_nodes(self, name, sub_idx=0, idx_list=[] ):
+ nodeCount = 0
+ for node in self.options.selected_nodes:
+ # The string with selected nodes looks like this:
+ # 'Object-id:subpath-index:node-index'
+ # example: 'path1419:0:1'
+ substr = node.split(":")
+ if name == substr[0]:
+ nodeCount += 1
+ if sub_idx == int(substr[1]):
+ idx_list.append(int(substr[2]))
+ return nodeCount
+
+
+ def process_node(self, elem):
+ if not isinstance(elem, inkex.PathElement):
+ return # just ignore all non-path elements
+
+ sub_idx = 0
+ hint = ""
+ for sub in elem.path.to_superpath():
+ # Calculate the objects rotation angle . For this,
+ # we assume a line between two nodes of the object.
+ # A horizontal line from left to right is 0 degrees.
+ # Positive angles means the line is rotated clockwise.
+ # -180 < alpha <= +180
+
+ # Select the two nodes to use for the calculation
+ node_idx = []
+ if self.count_selected_nodes( elem.get_id(), sub_idx, node_idx ) > 0:
+ # we have selected nodes from this element.
+ if len(node_idx) == 2:
+ # if the user selected exactly two individual nodes
+ # of a probably larger path, we shall use these.
+ p1_idx = node_idx[0]
+ p2_idx = node_idx[1]
+ hint = " (Segment defined by selected nodes)"
+ else:
+ # otherwise, we'll skip this sub-path and try the
+ # next from the same element.
+ sub_idx += 1
+ continue
+ else:
+ # if no nodes have been selected in this element, we
+ # use the start- and end-node of the sub-path
+ p1_idx = 0
+ p2_idx = -1
+
+ # Calculate angle, length, midpoint, etc.
+ x1=sub[p1_idx][1][0]
+ y1=sub[p1_idx][1][1]
+ x2=sub[p2_idx][1][0]
+ y2=sub[p2_idx][1][1]
+ width = x2-x1
+ heigth= y2-y1
+ if math.isclose( width, 0 ):
+ if math.isclose( heigth, 0 ):
+ alpha = 0
+ elif heigth > 0:
+ alpha = math.pi/2
+ else:
+ alpha = -math.pi/2
+ else:
+ alpha = math.atan(heigth/width)
+ if width < 0:
+ if heigth < 0:
+ alpha -= math.pi
+ else:
+ alpha += math.pi
+ xm = (x1+x2)/2
+ ym = (y1+y2)/2
+ length = math.sqrt( math.pow(width,2) + math.pow(heigth,2) )
+
+ # Select translation angle by options
+ if self.options.useFixedAngle:
+ da = math.radians(self.options.fixedAngle)
+ else:
+ da = alpha
+
+ # Calculate translation in x and y direction
+ # A positive distance value moves towards greater coordinates
+ dist = self.svg.unittouu('{}{}'.format(self.options.distance,
+ self.options.distUnit) )
+ if self.options.reverse:
+ da = da + math.radians(180)
+ dx = -math.sin(da) * dist
+ dy = math.cos(da) * dist
+
+ if self.options.tab == "translation":
+ if self.options.copyMode in ('copy'):
+ objToMove = elem.duplicate()
+ else:
+ if self.options.copyMode in ('obj'):
+ elem.duplicate()
+ objToMove = elem
+ objToMove.path = objToMove.path.translate( dx, dy )
+
+ elif self.options.tab == "align":
+ if self.options.reverseA:
+ alpha = alpha + math.radians(180)
+ self.align(xm, ym, alpha, length)
+
+ elif self.options.tab == "group":
+ if length > 0:
+ if self.options.reverseG:
+ alpha = alpha + math.radians(180)
+ self.make_group(elem.getparent(), xm, ym, alpha)
+
+ elif self.options.tab == "info":
+ msg = "Measuring result:"
+ self.msg(msg)
+ msg = " object name: {}"
+ self.msg(msg.format(elem.get_id()))
+ msg = " subpath: {} {}"
+ self.msg(msg.format(sub_idx, hint))
+ msg = " start point: x={} y={}"
+ self.msg(msg.format(x1,y1))
+ msg = " end point: x={} y={}"
+ self.msg(msg.format(x2,y2))
+ msg = " mid point: x={} y={}"
+ self.msg(msg.format(xm,ym))
+ msg = " object length: {}"
+ self.msg(msg.format(length))
+ msg = " object angle : {}°"
+ self.msg(msg.format(math.degrees(alpha)))
+ msg = " translation angle: {}°"
+ self.msg(msg.format(math.degrees(da)))
+ msg = " translation: dx={} dy={}"
+ self.msg(msg.format(dx,dy))
+ self.msg(" ")
+
+ # continue for-loop with next sub-index
+ sub_idx += 1
+
+
+ @staticmethod
+ def objectToPath(node):
+ if node.tag == inkex.addNS('g', 'svg'):
+ return node
+
+ if node.tag == inkex.addNS('path', 'svg') or node.tag == 'path':
+ for attName in node.attrib.keys():
+ if ("sodipodi" in attName) or ("inkscape" in attName):
+ del node.attrib[attName]
+ return node
+
+ return node
+
+
+ def recursiveFuseTransform(self, node, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
+
+ transf = Transform(transf) * Transform(node.get("transform", None))
+
+ if 'transform' in node.attrib:
+ del node.attrib['transform']
+
+ node = self.objectToPath(node)
+
+ if transf == NULL_TRANSFORM:
+ # Don't do anything if there is effectively no transform applied
+ # reduces alerts for unsupported nodes
+ pass
+ elif 'd' in node.attrib:
+ d = node.get('d')
+ p = CubicSuperPath(d)
+ p = Path(p).to_absolute().transform(transf, True)
+ node.set('d', str(Path(CubicSuperPath(p).to_path())))
+
+ elif node.tag in [inkex.addNS('polygon', 'svg'),
+ inkex.addNS('polyline', 'svg')]:
+ points = node.get('points')
+ points = points.strip().split(' ')
+ for k, p in enumerate(points):
+ if ',' in p:
+ p = p.split(',')
+ p = [float(p[0]), float(p[1])]
+ p = transf.apply_to_point(p)
+ p = [str(p[0]), str(p[1])]
+ p = ','.join(p)
+ points[k] = p
+ points = ' '.join(points)
+ node.set('points', points)
+
+ elif node.tag in [inkex.addNS("ellipse", "svg"), inkex.addNS("circle", "svg")]:
+
+ def isequal(a, b):
+ return abs(a - b) <= transf.absolute_tolerance
+
+ if node.TAG == "ellipse":
+ rx = float(node.get("rx"))
+ ry = float(node.get("ry"))
+ else:
+ rx = float(node.get("r"))
+ ry = rx
+
+ cx = float(node.get("cx"))
+ cy = float(node.get("cy"))
+ sqxy1 = (cx - rx, cy - ry)
+ sqxy2 = (cx + rx, cy - ry)
+ sqxy3 = (cx + rx, cy + ry)
+ newxy1 = transf.apply_to_point(sqxy1)
+ newxy2 = transf.apply_to_point(sqxy2)
+ newxy3 = transf.apply_to_point(sqxy3)
+
+ node.set("cx", (newxy1[0] + newxy3[0]) / 2)
+ node.set("cy", (newxy1[1] + newxy3[1]) / 2)
+ edgex = math.sqrt(
+ abs(newxy1[0] - newxy2[0]) ** 2 + abs(newxy1[1] - newxy2[1]) ** 2
+ )
+ edgey = math.sqrt(
+ abs(newxy2[0] - newxy3[0]) ** 2 + abs(newxy2[1] - newxy3[1]) ** 2
+ )
+
+ """
+ if not isequal(edgex, edgey) and (
+ node.TAG == "circle"
+ or not isequal(newxy2[0], newxy3[0])
+ or not isequal(newxy1[1], newxy2[1])
+ ):
+ inkex.utils.errormsg(
+ "Warning: Shape %s (%s) is approximate only, try Object to path first for better results"
+ % (node.TAG, node.get("id"))
+ )
+ """
+
+ if node.TAG == "ellipse":
+ node.set("rx", edgex / 2)
+ node.set("ry", edgey / 2)
+ else:
+ node.set("r", edgex / 2)
+
+ elif node.tag in [inkex.addNS('rect', 'svg'),
+ inkex.addNS('text', 'svg'),
+ inkex.addNS('image', 'svg'),
+ inkex.addNS('use', 'svg')]:
+ inkex.utils.errormsg(
+ "Shape %s (%s) not yet supported, try Object to path first"
+ % (node.TAG, node.get("id"))
+ )
+
+ for child in node.getchildren():
+ self.recursiveFuseTransform(child, transf)
+
+
+if __name__ == '__main__':
+ ParallelTranlation().run()