예제 #1
0
    def effect(self):
        scale = self.options.scale
        scaleFrom = self.options.scaleFrom

        tolerance = 10**(-1 * self.options.precision)

        printOut = False
        selections = self.svg.selected
        pathNodes = self.document.xpath('//svg:path', namespaces=inkex.NSS)
        outStrs = [str(len(pathNodes))]

        paths = [(pathNode.get('id'), CubicSuperPath(pathNode.get('d')))
                 for pathNode in pathNodes]

        if (len(paths) > 1):
            srcPath = paths[-1][1]
            srclen = self.getLength(srcPath, tolerance)
            paths = paths[:len(paths) - 1]
            for key, cspath in paths:
                curveLen = self.getLength(cspath, tolerance)

                self.scaleCubicSuper(cspath, scaleFactor = scale * (srclen / curveLen), \
                scaleFrom = scaleFrom)
                selections[key].set('d', CubicSuperPath(cspath))
        else:
            inkex.errormsg(
                _("Please select at least two paths, with the path whose \
            length is to be copied at the top. You may have to convert the shape \
            to path with path->Object to Path."))
def getCubicSuperPath(d = None):
    if(CommonDefs.inkVer == 1.0):
        if(d == None): return CubicSuperPath([])
        return CubicSuperPath(Path(d).to_superpath())
    else:
        if(d == None): return []
        return CubicSuperPath(simplepath.parsePath(d))
예제 #3
0
    def effect(self):
        selections = self.svg.selected
        pathNodes = self.document.xpath('//svg:path', namespaces=inkex.NSS)

        paths = {
            p.get('id'): getPartsFromCubicSuper(CubicSuperPath(p.get('d')))
            for p in pathNodes
        }

        #paths.keys() Order disturbed
        pathIds = [p.get('id') for p in pathNodes]

        if (len(paths) > 1):
            if (self.options.optimized):
                startPathId = pathIds[0]
                pathIds = getArrangedIds(paths, startPathId)

            newParts = []
            firstElem = None
            for key in pathIds:
                parts = paths[key]
                # ~ parts = getPartsFromCubicSuper(cspath)
                start = parts[0][0][0]
                try:
                    elem = self.svg.selected[key]

                    if (len(newParts) == 0):
                        newParts += parts[:]
                        firstElem = elem
                    else:
                        if (vectCmpWithMargin(start,
                                              newParts[-1][-1][-1],
                                              margin=.01)):
                            newParts[-1] += parts[0]
                        else:
                            newSeg = [
                                newParts[-1][-1][-1], newParts[-1][-1][-1],
                                start, start
                            ]
                            newParts[-1].append(newSeg)
                            newParts[-1] += parts[0]

                        if (len(parts) > 1):
                            newParts += parts[1:]

                    parent = elem.getparent()
                    idx = parent.index(elem)
                    parent.remove(elem)
                except:
                    pass  #elem might come from group item - in this case we need to ignore it

            newElem = copy.copy(firstElem)
            oldId = firstElem.get('id')
            newElem.set('d', CubicSuperPath(getCubicSuperFromParts(newParts)))
            newElem.set('id', oldId + '_joined')
            parent.insert(idx, newElem)
    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 = ApplyTransform.objectToPath(node)

        if 'd' in node.attrib:
            d = node.get('d')
            p = CubicSuperPath(d)
            p = Path(p).to_absolute().transform(transf, True)
            node.set('d', Path(CubicSuperPath(p).to_path()))

            self.scaleStrokeWidth(node, transf)

        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])]
                    applyTransformToPoint(transf, p)
                    p = [str(p[0]), str(p[1])]
                    p = ','.join(p)
                    points[k] = p
            points = ' '.join(points)
            node.set('points', points)

            self.scaleStrokeWidth(node, transf)

        elif node.tag in [
                inkex.addNS('rect', 'svg'),
                inkex.addNS('text', 'svg'),
                inkex.addNS('image', 'svg'),
                inkex.addNS('use', 'svg'),
                inkex.addNS('circle', 'svg')
        ]:
            # node.set('transform', str(Transform(transf)))
            inkex.utils.errormsg("Shape %s not yet supported" % node.tag)

        else:
            # e.g. <g style="...">
            self.scaleStrokeWidth(node, transf)

        for child in node.getchildren():
            self.recursiveFuseTransform(child, transf)
예제 #5
0
def splitPath(inkex, node):
    dashes = []
    try:  # inkscape 1.0
        style = dict(Style(node.get('style')))
    except:  # inkscape 0.9x
        style = simplestyle.parseStyle(node.get('style'))
    if 'stroke-dasharray' in style:
        if style['stroke-dasharray'].find(',') > 0:
            dashes = [
                float(dash) for dash in style['stroke-dasharray'].split(',')
                if dash
            ]
    if dashes:
        try:  # inkscape 1.0
            p = CubicSuperPath(node.get('d'))
        except:  # inkscape 0.9x
            p = cubicsuperpath.parsePath(node.get('d'))
        new = []
        for sub in p:
            idash = 0
            dash = dashes[0]
            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
        try:  # inkscape 1.0
            node.set('d', CubicSuperPath(new))
        except:  # inkscape 0.9x
            node.set('d', cubicsuperpath.formatPath(new))
        del style['stroke-dasharray']
        try:  # inkscape 1.0
            node.set('style', Style(style))
        except:  # inkscape 0.9x
            node.set('style', simplestyle.formatStyle(style))
        if node.get(inkex.addNS('type', 'sodipodi')):
            del node.attrib[inkex.addNS('type', 'sodipodi')]
예제 #6
0
 def test_from_arrays(self):
     """SuperPath from arrays"""
     csp = CubicSuperPath([[
         [[14, 173], [14, 173], (14, 173)],
         [(15, 171), (17, 168), (18, 168)],
     ],
                           [
                               [(18, 167), (18, 167), [20, 165]],
                               ((21, 164), [22, 162], (23, 162)),
                           ]])
     self.assertEqual(
         str(csp.to_path()),
         'M 14 173 C 14 173 15 171 17 168 M 18 167 C 20 165 21 164 22 162')
예제 #7
0
    def effect(self):
        unit_factor = 1.0 / self.svg.uutounit(1.0, self.options.unit)
        for id, node in self.svg.selected.items():

            #get recent XY coordinates (top left corner of the bounding box)
            bbox = node.bounding_box()
            new_horiz_scale = self.options.expected_size * unit_factor / bbox.width
            new_vert_scale = self.options.expected_size * unit_factor / bbox.height

            if self.options.scale_type == "Horizontal":
                translation_matrix = [[new_horiz_scale, 0.0, 0.0],
                                      [0.0, 1.0, 0.0]]
            elif self.options.scale_type == "Vertical":
                translation_matrix = [[1.0, 0.0, 0.0],
                                      [0.0, new_vert_scale, 0.0]]
            else:  #Uniform
                translation_matrix = [[new_horiz_scale, 0.0, 0.0],
                                      [0.0, new_vert_scale, 0.0]]
            node.transform = Transform(translation_matrix) * node.transform

            # now that the node moved we need to get the nodes XY coordinates again to fix them
            bbox_new = node.bounding_box()

            #inkex.utils.debug(cx)
            #inkex.utils.debug(cy)
            #inkex.utils.debug(cx_new)
            #inkex.utils.debug(cy_new)

            # We remove the transformation attribute from SVG XML tree in case it's regular path. In case the node is an object like arc,circle, ellipse or star we have the attribute "sodipodi:type". Then we do not play with it's path because the size transformation will not apply (this code block is ugly)
            if node.get('sodipodi:type') is None and 'd' in node.attrib:
                #inkex.utils.debug("it's a path!")
                d = node.get('d')
                p = CubicSuperPath(d)
                transf = Transform(node.get("transform", None))
                if 'transform' in node.attrib:
                    del node.attrib['transform']
                p = Path(p).to_absolute().transform(transf, True)
                node.set('d', Path(CubicSuperPath(p).to_path()))
            #else:
            #inkex.utils.debug("it's an object!")

            #we perform second translation to reset the center of the path
            translation_matrix = [[
                1.0, 0.0,
                bbox.left - bbox_new.left + (bbox.width - bbox_new.width) / 2
            ],
                                  [
                                      0.0, 1.0, bbox.top - bbox_new.top +
                                      (bbox.height - bbox_new.height) / 2
                                  ]]
            node.transform = Transform(translation_matrix) * node.transform
예제 #8
0
    def effect(self):
        path_strings = []
        net_strings = ["M"]
        my_path = etree.Element(inkex.addNS('path', 'svg'))
        s = {
            'stroke-width': self.options.s_width,
            'stroke': '#000000',
            'fill': 'none'
        }
        my_path.set('style', str(inkex.Style(s)))
        for id, node in self.svg.selected.items():
            if node.tag == inkex.addNS('path', 'svg'):
                d = node.get('d')
                p = CubicSuperPath(Path(d))
                for subpath in p:
                    for i, csp in enumerate(subpath):
                        path_strings.append("%f,%f" % (csp[1][0], csp[1][1]))
                node.set('d', str(Path(p)))

                while len(path_strings) > 0:
                    net_strings.append(path_strings.pop(0))
                    if len(path_strings) > 0:
                        net_strings.append(path_strings.pop())
                my_path.set('d', " ".join(net_strings))
                self.svg.get_current_layer().append(my_path)
    def effect(self):

        if len(self.svg.selected) == 0:
            inkex.errormsg(
                _("Please select an object to perform the " +
                  "exponential-distort transformation on."))
            return

        for id, node in self.svg.selected.items():
            type = node.get(
                "{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}type",
                "path")
            if node.tag != '{http://www.w3.org/2000/svg}path' or type != 'path':
                inkex.errormsg(node.tag + " is not a path. Type=" + type +
                               ". Please use 'Path->Object to Path' first.")
            else:
                pts = CubicSuperPath(node.get('d'))
                bbox = self.computeBBox(pts)
                ## bbox (60.0, 160.0, 77.0, 197.0)
                ## pts [[[[60.0, 77.0], [60.0, 77.0], [60.0, 77.0]], [[60.0, 197.0], [60.0, 197.0], [60.0, 197.0]], [[70.0, 197.0], ...
                for p in pts:
                    for pp in p:
                        for ppp in pp:
                            ppp[0] = self.x_exp_p_inplace(bbox, ppp[0])

                node.set('d', str(pts))
예제 #10
0
    def breakContours(self, node):  #this does the same as "CTRL + SHIFT + K"
        if node.tag == inkex.addNS('path', 'svg'):
            parent = node.getparent()
            idx = parent.index(node)
            idSuffix = 0
            raw = Path(node.get("d")).to_arrays()
            subPaths, prev = [], 0
            for i in range(
                    len(raw)):  # Breaks compound paths into simple paths
                if raw[i][0] == 'M' and i != 0:
                    subPaths.append(raw[prev:i])
                    prev = i
            subPaths.append(raw[prev:])
            for subpath in subPaths:
                replacedNode = copy.copy(node)
                oldId = replacedNode.get('id')

                replacedNode.set('d', CubicSuperPath(subpath))
                replacedNode.set('id', oldId + str(idSuffix).zfill(5))
                parent.insert(idx, replacedNode)
                idSuffix += 1
                self.replacedNodes.append(replacedNode)
            node.delete()
        for child in node:
            self.breakContours(child)
예제 #11
0
 def path_round_it(self, node):
     if node.tag == inkex.addNS('path', 'svg'):
         d = node.get('d')
         p = CubicSuperPath(d)
         for subpath in p:
             for csp in subpath:
                 delta = self.roundit(csp[1])
                 if self.options.along:
                     csp[0][0] += delta[0]
                     csp[0][1] += delta[1]
                 csp[1][0] += delta[0]
                 csp[1][1] += delta[1]
                 if self.options.along:
                     csp[2][0] += delta[0]
                     csp[2][1] += delta[1]
                 if self.options.ctrl:
                     delta = self.roundit(csp[0])
                     csp[0][0] += delta[0]
                     csp[0][1] += delta[1]
                     delta = self.roundit(csp[2])
                     csp[2][0] += delta[0]
                     csp[2][1] += delta[1]
         node.set('d', str(Path(p)))
     elif node.tag == inkex.addNS('g', 'svg'):
         for e in node:
             self.path_round_it(e)
예제 #12
0
 def test_AZ(self):
     p = [
         ['M', [0., 4.]],
         ['A', [3., 6., 0., 1, 1, 5., 4.]],
         ['Z', []],
     ]
     csp = CubicSuperPath(p)
     self.assertTrue(len(csp[0]) > 3)
예제 #13
0
 def create_vert_blocks(self, group, gridx, style):
     path = lastpath = 0
     blocks = []
     count = 0
     for node in gridx.iterchildren():
         if node.tag == inkex.addNS('path','svg'): # which they ALL should because we just made them
             path = CubicSuperPath(node.get('d')) # turn it into a global C style SVG path
             #sys.stderr.write("count: %d\n"% count)
             if count == 0: # first one so use the right border
                 spath = node.get('d') # work on string instead of cubicpath
                 lastpoint = spath.split()[-2:]
                 lastx = float(lastpoint[0])
                 lasty = float(lastpoint[1])
                 #sys.stderr.write("lastpoint: %s\n"% lastpoint)
                 spath += ' %f %f %f %f %f %f' % (lastx+self.inner_radius/2,lasty, self.width-1.5*self.inner_radius,lasty, self.width-self.inner_radius, lasty)
                 spath += ' %f %f %f %f %f %f' % (self.width-self.inner_radius/2,lasty, self.width,self.height-self.inner_radius/2, self.width,self.height-self.inner_radius)
                 spath += ' %f %f %f %f %f %f' % (self.width,self.height-1.5*self.inner_radius, self.width, 1.5*self.inner_radius, self.width,self.inner_radius)
                 spath += ' %f %f %f %f %f %f' % (self.width,self.inner_radius/2, self.width-self.inner_radius/2,0, self.width-self.inner_radius,0)
                 spath += 'z'
                 #sys.stderr.write("spath: %s\n"% spath)
                 #
                 name = "ColPieces_%d" % (count)
                 attribs = { 'style':style, 'id':name, 'd':spath }
                 n = etree.SubElement(group, inkex.addNS('path','svg'), attribs )
                 blocks.append(n) # for direct traversal later
             else: # internal line - concat a reversed version with the last one
                 thispath = copy.deepcopy(path)
                 for i in range(len(thispath[0])): # reverse the internal C pairs
                     thispath[0][i].reverse()
                 thispath[0].reverse() # reverse the entire line
                 lastpath[0].extend(thispath[0]) # append
                 name = "ColPieces_%d" % (count)
                 attribs = { 'style':style, 'id':name, 'd':dirtyFormat(lastpath) }
                 n = etree.SubElement(group, inkex.addNS('path','svg'), attribs )
                 blocks.append(n) # for direct traversal later
                 n.set('d', n.get('d')+'z') # close it
             #
             count += 1
             lastpath = path
     # do the last one (LHS)
     spath = node.get('d') # work on string instead of cubicpath
     lastpoint = spath.split()[-2:]
     lastx = float(lastpoint[0])
     lasty = float(lastpoint[1])
     #sys.stderr.write("lastpoint: %s\n"% lastpoint)
     spath += ' %f %f %f %f %f %f' % (lastx-self.inner_radius,lasty, 1.5*self.inner_radius, lasty, self.inner_radius,lasty)
     spath += ' %f %f %f %f %f %f' % (self.inner_radius/2,lasty, 0,lasty-self.inner_radius/2, 0,lasty-self.inner_radius)
     spath += ' %f %f %f %f %f %f' % (0,lasty-1.5*self.inner_radius, 0,1.5*self.inner_radius, 0,self.inner_radius)
     spath += ' %f %f %f %f %f %f' % (self.inner_radius/2,0, self.inner_radius,0, 1.5*self.inner_radius, 0)
     spath += 'z'
     #
     name = "ColPieces_%d" % (count)
     attribs = { 'style':style, 'id':name, 'd':spath }
     n = etree.SubElement(group, inkex.addNS('path','svg'), attribs )
     blocks.append(n) # for direct traversal later
     #
     return(blocks)
예제 #14
0
    def addPathVertices(self,
                        path,
                        node=None,
                        transform=None,
                        clone_transform=None):
        """
        Decompose the path data from an SVG element into individual
        subpaths, each subpath consisting of absolute move to and line
        to coordinates.  Place these coordinates into a list of polygon
        vertices.
        """

        if (not path) or (len(path) == 0):
            # Nothing to do
            return

        sp = Path(path)
        if (not sp) or (len(sp) == 0):
            # Path must have been devoid of any real content
            return

        # Get a cubic super path
        p = CubicSuperPath(sp)
        if (not p) or (len(p) == 0):
            # Probably never happens, but...
            return

        # Now traverse the cubic super path
        subpath_list = []
        subpath_vertices = []
        for sp in p:
            if len(subpath_vertices):
                # There's a prior subpath: see if it is closed and should be saved
                if distanceSquared(subpath_vertices[0],
                                   subpath_vertices[-1]) < 1:
                    # Keep the prior subpath: it appears to be a closed path
                    subpath_list.append(subpath_vertices)
            subpath_vertices = []
            subdivideCubicPath(sp, 0.2)
            for csp in sp:
                # Add this vertex to the list of vertices
                subpath_vertices.append(csp[1])

        # Handle final subpath
        if len(subpath_vertices):
            if distanceSquared(subpath_vertices[0], subpath_vertices[-1]) < 1:
                # Path appears to be closed so let's keep it
                subpath_list.append(subpath_vertices)

        # Empty path?
        if not subpath_list:
            return

        # Store the list of subpaths in a dictionary keyed off of the path's node pointer
        self.paths[node] = subpath_list
        self.paths_clone_transform[node] = clone_transform
예제 #15
0
    def envelope2coords(self, path_envelope):
        pp_envelope = CubicSuperPath(path_envelope)
        # inkex.debug(pp_envelope)

        c0 = pp_envelope[0][0][0]
        c1 = pp_envelope[0][1][0]
        c2 = pp_envelope[0][2][0]
        c3 = pp_envelope[0][3][0]
        # inkex.debug(str(c0)+" "+str(c1)+" "+str(c2)+" "+str(c3))
        return [c0, c1, c2, c3]
예제 #16
0
    def split_into_nodes(self, nodes_number=1000):
        for id, node in self.svg.selected.items():
            if node.tag == inkex.addNS('path', 'svg'):
                p = CubicSuperPath(node.get('d'))
                new = []
                for sub in p:
                    new.append([sub[0][:]])
                    i = 1
                    while i <= len(sub) - 1:
                        length = bezier.cspseglength(new[-1][-1], sub[i])

                        splits = nodes_number
                        for s in range(int(splits), 1, -1):
                            new[-1][-1], next, sub[
                                i] = bezier.cspbezsplitatlength(
                                    new[-1][-1], sub[i], 1.0 / s)
                            new[-1].append(next[:])
                        new[-1].append(sub[i])
                        i += 1
                node.set('d', str(CubicSuperPath(new)))
예제 #17
0
    def _sekl_call(self, skeletons, p0, dx, bbox):
        if self.options.vertical:
            flipxy(p0)
        newp = []
        for skelnode in skeletons.values():
            self.curSekeleton = skelnode.path.to_superpath()
            if self.options.vertical:
                flipxy(self.curSekeleton)
            for comp in self.curSekeleton:
                path = copy.deepcopy(p0)
                self.skelcomp, self.lengths = linearize(comp)
                # !!!!>----> TODO: really test if path is closed! end point==start point is not enough!
                self.skelcompIsClosed = (self.skelcomp[0] == self.skelcomp[-1])

                length = sum(self.lengths)
                xoffset = self.skelcomp[0][
                    0] - bbox.x.minimum + self.options.toffset
                yoffset = self.skelcomp[0][
                    1] - bbox.y.center - self.options.noffset

                if self.options.repeat:
                    NbCopies = max(
                        1, int(round((length + self.options.space) / dx)))
                    width = dx * NbCopies
                    if not self.skelcompIsClosed:
                        width -= self.options.space
                    bbox.x.maximum = bbox.x.minimum + width
                    new = []
                    for sub in path:
                        for _ in range(NbCopies):
                            new.append(copy.deepcopy(sub))
                            offset(sub, dx, 0)
                    path = new

                for sub in path:
                    offset(sub, xoffset, yoffset)

                if self.options.stretch:
                    if not bbox.width:
                        raise inkex.AbortExtension(
                            "The 'stretch' option requires that the pattern must have non-zero width :\nPlease edit the pattern width."
                        )
                    for sub in path:
                        stretch(sub, length / bbox.width, 1, self.skelcomp[0])

                for sub in path:
                    for ctlpt in sub:
                        self.apply_diffeomorphism(ctlpt[1],
                                                  (ctlpt[0], ctlpt[2]))

                if self.options.vertical:
                    flipxy(path)
                newp += path
        return CubicSuperPath(newp)
예제 #18
0
 def test_QT(self):
     p = [
         ['M', [0.0, 0.0]],
         ['Q', [3.0, 0.0, 3.0, 3.0]],
         ['T', [0.0, 6.0]],
     ]
     csp = CubicSuperPath(p)
     self.assertDeepAlmostEqual(csp, [[
         [[0.0, 0.0], [0.0, 0.0], [2.0, 0.0]],
         [[3.0, 1.0], [3.0, 3.0], [3.0, 5.0]],
         [[2.0, 6.0], [0.0, 6.0], [0.0, 6.0]],
     ]])
예제 #19
0
 def test_CS(self):
     p = [
         ['M', [1.2, 2.3]],
         ['C', [4.5, 3.4, 5.6, 6.7, 8.9, 7.8]],
         ['S', [9.1, 1.2, 2.3, 3.4]],
     ]
     csp = CubicSuperPath(p)
     self.assertDeepAlmostEqual(csp, [[
         [[1.2, 2.3], [1.2, 2.3], [4.5, 3.4]],
         [[5.6, 6.7], [8.9, 7.8], [12.2, 8.9]],
         [[9.1, 1.2], [2.3, 3.4], [2.3, 3.4]],
     ]])
예제 #20
0
 def effect(self):
     if len(self.svg.selected) > 0:
         output_all = output_nodes = ""
         for node in self.svg.selection.filter(inkex.PathElement):
             node.apply_transform()
             p = CubicSuperPath(node.get('d'))
             for subpath in p:
                 for csp in subpath:
                     output_nodes += str(csp[1][0]) + "\t" + str(csp[1][1]) + "\n"
             output_nodes += "\n"                 
         sys.stderr.write(output_nodes.strip())
     else:
         inkex.errormsg('Please select some paths first.')
         return
예제 #21
0
    def envelope2coords(self, path_envelope):
        pp_envelope = CubicSuperPath(path_envelope)
        if len(pp_envelope[0]) < 4:
            inkex.errormsg(
                "The selected envelope (your second path) does not contain enough nodes. Check to have at least 4 nodes."
            )
            exit()

        c0 = pp_envelope[0][0][0]
        c1 = pp_envelope[0][1][0]
        c2 = pp_envelope[0][2][0]
        c3 = pp_envelope[0][3][0]
        # inkex.debug(str(c0)+" "+str(c1)+" "+str(c2)+" "+str(c3))
        return [c0, c1, c2, c3]
예제 #22
0
 def test_LHV(self):
     p = [
         ['M', [1.2, 2.3]],
         ['L', [3.4, 4.5]],
         ['H', [5.6]],
         ['V', [6.7]],
     ]
     csp = CubicSuperPath(p)
     self.assertDeepAlmostEqual(csp, [[
         [[1.2, 2.3], [1.2, 2.3], [1.2, 2.3]],
         [[3.4, 4.5], [3.4, 4.5], [3.4, 4.5]],
         [[5.6, 4.5], [5.6, 4.5], [5.6, 4.5]],
         [[5.6, 6.7], [5.6, 6.7], [5.6, 6.7]],
     ]])
예제 #23
0
    def effect(self):
    
        if len(self.svg.selected) > 0:
            global output_nodes, points
            
            #create numpy array of nodes
            n_array = []
                    
            #Iterate through all the selected objects in Inkscape
            for  node in self.svg.selected.values():
                #Check if the node is a path ("svg:path" node in XML )
                #id = node.id
                if node.tag == inkex.addNS('path','svg'):
                    # bake (or fuse) transform
                    node.apply_transform()
                    #turn into cubicsuperpath
                    d = node.get('d')
                    p = CubicSuperPath(d)
                    for subpath in p: # there may be several paths joined together (e.g. holes)
                        for csp in subpath: # groups of three to handle control points.
                            # just the points no control points (handles)
                            n_array.append(csp[1][0])
                            n_array.append(csp[1][1])

            k = n.asarray(n_array)
            length = int(len(k)/2)
            c = k.reshape(length,2)
            hull_pts = qhull(c)
            
            pdata = ''
            for vertex in hull_pts:
                if pdata == '':
                    pdata = 'M%f,%f' % (vertex[0], vertex[1] )
                else:
                    pdata += 'L %f,%f' %  (vertex[0], vertex[1] )
            pdata += ' Z'        
            path = 'polygon'
            makeGroup = False
            paths_clone_transform = None
            self.joinWithNode(path, pdata, makeGroup, paths_clone_transform)
            
        else:
            inkex.errormsg('Please select some paths first.')
            return
예제 #24
0
def getCubicSuperFromParts(parts):
    cbsuper = []
    for part in parts:
        subpath = []
        lastPt = None
        pt = None
        for seg in part:
            if (pt == None):
                ptLeft = seg[0]
                pt = seg[0]
            ptRight = seg[1]
            subpath.append([ptLeft, pt, ptRight])
            ptLeft = seg[2]
            pt = seg[3]
        subpath.append([ptLeft, pt, pt])
        cbsuper.append(subpath)
    if (ver == 1.0):
        return CubicSuperPath(cbsuper)
    else:
        return cbsuper
예제 #25
0
    def recursiveFuseTransform(self,
                               node,
                               transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
        # Modification by David Burghoff:
        # Since transforms apply to an object's clips, before applying the transform
        # we will need to duplicate the clip path and transform it
        clippathurl = node.get('clip-path')
        if clippathurl is not None and node.get("transform") is not None:
            myn = node
            while not (myn.getparent() == None):  # get svg handle
                myn = myn.getparent()
            svg = myn
            clippath = svg.getElementById(clippathurl[5:-1])
            if clippath is not None:
                d = clippath.duplicate()
                clippathurl = "url(#" + d.get("id") + ")"
                node.set("clip-path", clippathurl)
                for k in d.getchildren():
                    if k.get('transform') is not None:
                        tr = Transform(node.get("transform")) * Transform(
                            k.get('transform'))
                    else:
                        tr = Transform(node.get("transform"))
                    k.set('transform', tr)

        transf = Transform(transf) * Transform(node.get("transform", None))

        if 'transform' in node.attrib:
            del node.attrib['transform']

        node = ApplyTransform.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())))

            self.scaleStrokeWidth(node, transf)

        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)

            self.scaleStrokeWidth(node, transf)

        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)

        # Modficiation by David Burghoff: Added support for lines
        elif node.tag in inkex.addNS('line', 'svg'):
            x1 = node.get('x1')
            x2 = node.get('x2')
            y1 = node.get('y1')
            y2 = node.get('y2')
            p1 = transf.apply_to_point([x1, y1])
            p2 = transf.apply_to_point([x2, y2])
            node.set('x1', str(p1[0]))
            node.set('y1', str(p1[1]))
            node.set('x2', str(p2[0]))
            node.set('y2', str(p2[1]))
            self.scaleStrokeWidth(node, transf)

        elif node.typename in ['Rectangle']:
            x = float(node.get('x'))
            y = float(node.get('y'))
            w = float(node.get('width'))
            h = float(node.get('height'))
            pts = [[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]]
            xs = []
            ys = []
            for p in pts:
                p = transf.apply_to_point(p)
                xs.append(p.x)
                ys.append(p.y)

            node.set('x', str(min(xs)))
            node.set('y', str(min(ys)))
            node.set('width', str(max(xs) - min(xs)))
            node.set('height', str(max(ys) - min(ys)))
            self.scaleStrokeWidth(node, transf)

        elif node.tag in [
                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")))

        else:
            # e.g. <g style="...">
            self.scaleStrokeWidth(node, transf)

        for child in node.getchildren():
            self.recursiveFuseTransform(child, transf)
예제 #26
0
    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 = ApplyTransform.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', Path(CubicSuperPath(p).to_path()))

            self.scaleStrokeWidth(node, transf)

        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)

            self.scaleStrokeWidth(node, transf)

        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")))

        else:
            # e.g. <g style="...">
            self.scaleStrokeWidth(node, transf)

        for child in node.getchildren():
            self.recursiveFuseTransform(child, transf)
예제 #27
0
    def effect(self):
        if len(self.options.ids) != 2:
            inkex.errormsg(
                "This extension requires two selected objects. The first selected object must be the straight line with two nodes."
            )
            exit()

        # drawing that will be scaled is selected second, must be a single object
        scalepath = self.svg.selected[self.options.ids[0]]
        drawing = self.svg.selected[self.options.ids[1]]

        if scalepath.tag != inkex.addNS('path', 'svg'):
            inkex.errormsg(
                "The first selected object is not a path.\nPlease select a straight line with two nodes instead."
            )
            exit()

        # apply its transforms to the scaling path, so we get the correct coordinates to calculate path length
        scalepath.apply_transform()

        path = CubicSuperPath(scalepath.get('d'))
        if len(path) < 1 or len(path[0]) < 2:
            inkex.errormsg(
                "This extension requires that the first selected path be two nodes long."
            )
            exit()

        # calculate path length
        p1_x = path[0][0][1][0]
        p1_y = path[0][0][1][1]
        p2_x = path[0][1][1][0]
        p2_y = path[0][1][1][1]

        p_length = self.svg.unittouu(
            str(distance((p1_x, p1_y), (p2_x, p2_y))) + self.svg.unit)

        # Find Drawing Scale
        if self.options.choosescale == 'metric':
            drawing_scale = int(self.options.metric)
        elif self.options.choosescale == 'imperial':
            drawing_scale = int(self.options.imperial)
        elif self.options.choosescale == 'custom':
            drawing_scale = self.options.custom_scale

        # calculate scaling center
        center = self.calc_scale_center(p1_x, p1_y, p2_x, p2_y)

        # calculate scaling factor
        target_length = self.svg.unittouu(
            str(self.options.length) + self.options.unit)
        factor = (target_length / p_length) / drawing_scale
        # inkex.debug("%s, %s  %s" % (target_length, p_length, factor))

        # Add scale ruler
        if self.options.showscale == "true":
            dist = int(self.options.unitlength)

            ruler_length = self.svg.unittouu(str(dist) +
                                             self.options.unit) / drawing_scale
            ruler_pos = (p1_x + (p2_x - p1_x) / 2, (p1_y + (p2_y - p1_y) / 2) -
                         self.svg.unittouu('4 mm'))

            # TODO: add into current layer instead
            self.create_ruler(self.document.getroot(), ruler_length, ruler_pos,
                              dist, drawing_scale)

        # Get drawing and current transformations
        for obj in (scalepath, drawing):
            # Scale both objects about the center, first translate back to origin
            scale_matrix = [[1, 0.0, -center[0]], [0.0, 1, -center[1]]]
            obj.transform = Transform(scale_matrix) * obj.transform
            # Then scale
            scale_matrix = [[factor, 0.0, 0.0], [0.0, factor, 0.0]]
            obj.transform = Transform(scale_matrix) * obj.transform
            # Then translate back to original scale center location
            scale_matrix = [[1, 0.0, center[0]], [0.0, 1, center[1]]]
            obj.transform = Transform(scale_matrix) * obj.transform
예제 #28
0
    def effect(self):
        # Check that elements have been selected
        if len(self.options.ids) == 0:
            inkex.errormsg("Please select objects!")
            return

        # Drawing styles
        linestyle = {
            'stroke': '#000000',
            'stroke-width': str(self.svg.unittouu('1px')),
            'fill': 'none'
        }

        facestyle = {
            'stroke': '#000000',
            'stroke-width': '0px',  # str(self.svg.unittouu('1px')),
            'fill': 'none'
        }

        # Handle the transformation of the current group
        parentGroup = (self.svg.selected[self.options.ids[0]]).getparent()

        svg = self.document.getroot()
        image_element = svg.find('.//{http://www.w3.org/2000/svg}image')
        if image_element is None:
            inkex.utils.debug("No image found")
            exit(1)
        self.path = self.checkImagePath(
            image_element)  # This also ensures the file exists
        if self.path is None:  # check if image is embedded or linked
            image_string = image_element.get(
                '{http://www.w3.org/1999/xlink}href')
            # find comma position
            i = 0
            while i < 40:
                if image_string[i] == ',':
                    break
                i = i + 1
            img = Image.open(
                BytesIO(base64.b64decode(image_string[i +
                                                      1:len(image_string)])))
        else:
            img = Image.open(self.path)

        extrinsic_image_width = float(image_element.get('width'))
        extrinsic_image_height = float(image_element.get('height'))
        (width, height) = img.size
        trans = self.getGlobalTransform(parentGroup)
        invtrans = None
        if trans:
            invtrans = self.invertTransform(trans)

        # Recovery of the selected objects
        pts = []
        nodes = []
        seeds = []

        for id in self.options.ids:
            node = self.svg.selected[id]
            nodes.append(node)
            if (node.tag == "{http://www.w3.org/2000/svg}path"
                ):  #If it is path
                # Get vertex coordinates of path
                points = CubicSuperPath(node.get('d'))
                for p in points[0]:
                    pt = [p[1][0], p[1][1]]
                    if trans:
                        Transform(trans).apply_to_point(pt)
                    pts.append(Point(pt[0], pt[1]))
                    seeds.append(Point(p[1][0], p[1][1]))
            else:  # For other shapes
                bbox = node.bounding_box()
                if bbox:
                    cx = 0.5 * (bbox.left + bbox.top)
                    cy = 0.5 * (bbox.top + bbox.bottom)
                    pt = [cx, cy]
                    if trans:
                        Transform(trans).apply_to_point(pt)
                    pts.append(Point(pt[0], pt[1]))
                    seeds.append(Point(cx, cy))
        pts.sort()
        seeds.sort()

        # In Creation of groups to store the result
        # Delaunay
        groupDelaunay = etree.SubElement(parentGroup, inkex.addNS('g', 'svg'))
        groupDelaunay.set(inkex.addNS('label', 'inkscape'), 'Delaunay')

        scale_x = float(extrinsic_image_width) / float(width)
        scale_y = float(extrinsic_image_height) / float(height)
        # Voronoi diagram generation

        triangles = voronoi.computeDelaunayTriangulation(seeds)
        for triangle in triangles:
            p1 = seeds[triangle[0]]
            p2 = seeds[triangle[1]]
            p3 = seeds[triangle[2]]
            cmds = [['M', [p1.x, p1.y]], ['L', [p2.x, p2.y]],
                    ['L', [p3.x, p3.y]], ['Z', []]]
            path = etree.Element(inkex.addNS('path', 'svg'))
            path.set('d', str(Path(cmds)))
            middleX = (p1.x + p2.x + p3.x) / 3.0
            middleY = (p1.y + p2.y + p3.y) / 3.0
            if width > middleX and height > middleY and middleX >= 0 and middleY >= 0:
                pixelColor = img.getpixel((middleX, middleY))
                facestyle["fill"] = str(
                    inkex.Color((pixelColor[0], pixelColor[1], pixelColor[2])))
            else:
                facestyle["fill"] = "black"
            path.set('style', str(inkex.Style(facestyle)))
            groupDelaunay.append(path)
예제 #29
0
def getParsedPath(d):
    if (ver == 1.0):
        return CubicSuperPath(Path(d).to_superpath())
    else:
        return cubicsuperpath.parsePath(d)
예제 #30
0
    def effect(self):
        if self.options.version:
            print(__version__)
            sys.exit(0)

        self.calc_unit_factor(self.options.units)

        if self.options.snap_ends is not None:
            self.snap_ends = self.options.snap_ends
        if self.options.close_loops is not None:
            self.close_loops = self.options.close_loops
        if self.options.chain_epsilon is not None:
            self.chain_epsilon = self.options.chain_epsilon
        if self.chain_epsilon < 0.001:
            self.chain_epsilon = 0.001  # keep a minimum.
        self.eps_sq = self.chain_epsilon * self.unit_factor * self.chain_epsilon * self.unit_factor

        if not len(self.svg.selected.items()):
            inkex.errormsg("Please select one or more objects.")
            return

        segments = []
        for id, node in self.svg.selected.items():
            if node.tag != inkex.addNS('path', 'svg'):
                inkex.errormsg(
                    "Object " + id +
                    " is not a path. Try\n  - Path->Object to Path\n  - Object->Ungroup"
                )
                return
            if debug:
                inkex.utils.debug("id=" + str(id) + ", tag=" + str(node.tag))
            path_d = CubicSuperPath(Path(node.get('d')))
            sub_idx = -1
            for sub in path_d:
                sub_idx += 1
                # sub = [[[200.0, 300.0], [200.0, 300.0], [175.0, 290.0]], [[175.0, 265.0], [220.37694, 256.99876], [175.0, 240.0]], [[175.0, 215.0], [200.0, 200.0], [200.0, 200.0]]]
                # this is a path of three points. All the bezier handles are included. the Structure is:
                # [[handle0_OUT, point0, handle0_1], [handle1_0, point1, handle1_2], [handle2_1, point2, handle2_OUT]]
                # the _OUT handles at the end of the path are ignored. The data structure has them identical to their points.
                #
                if debug: inkex.utils.debug("   sub=" + str(sub))
                end1 = [sub[0][1][0], sub[0][1][1]]
                end2 = [sub[-1][1][0], sub[-1][1][1]]

                # Remove trivial self revesal when building candidate segments list.
                if ((len(sub) == 3) and self.near_ends(end1, end2)):
                    if debug:
                        inkex.utils.debug(
                            "dropping segment from self-reversing path, length:"
                            + str(len(sub)))
                    sub.pop()
                    end2 = [sub[-1][1][0], sub[-1][1][1]]

                segments.append({
                    'id': id,
                    'n': sub_idx,
                    'end1': end1,
                    'end2': end2,
                    'seg': sub
                })
            if node.get(inkex.addNS('type', 'sodipodi')):
                del node.attrib[inkex.addNS('type', 'sodipodi')]
        if debug: inkex.utils.debug("-------- seen: ")
        for s in segments:
            if debug:
                inkex.utils.debug(
                    str(s['id']) + ", " + str(s['n']) + ", " + str(s['end1']) +
                    ", " + str(s['end2']))

        # chain the segments
        obsoleted = 0
        remaining = 0
        for id, node in self.svg.selected.items():
            path_d = CubicSuperPath(Path(node.get('d')))
            # ATTENTION: for parsePath() it is the same, if first and last point coincide, or if the path is really closed.
            path_closed = True if re.search("z\s*$", node.get('d')) else False
            new = []
            cur_idx = -1
            for chain in path_d:
                cur_idx += 1
                if not self.is_segment_done(id, cur_idx):
                    # quadratic algorithm: we check both ends of the current segment.
                    # If one of them is near another known end from the segments list, we
                    # chain this segment to the current segment and remove it from the
                    # list,
                    # end1-end1 or end2-end2: The new segment is reversed.
                    # end1-end2: The new segment is prepended to the current segment.
                    # end2-end1: The new segment is appended to the current segment.
                    self.set_segment_done(
                        id, cur_idx, "output")  # do not cross with ourselves.
                    end1 = [chain[0][1][0], chain[0][1][1]]
                    end2 = [chain[-1][1][0], chain[-1][1][1]]

                    # Remove trivial self revesal when doing the actual chain operation.
                    if ((len(chain) == 3) and self.near_ends(end1, end2)):
                        chain.pop()
                        end2 = [chain[-1][1][0], chain[-1][1][1]]

                    segments_idx = 0
                    while segments_idx < len(segments):
                        seg = segments[segments_idx]
                        if self.is_segment_done(seg['id'], seg['n']):
                            segments_idx += 1
                            continue

                        if (self.near_ends(end1, seg['end1'])
                                or self.near_ends(end2, seg['end2'])):
                            seg['seg'] = self.reverse_segment(seg['seg'])
                            seg['end1'], seg['end2'] = seg['end2'], seg['end1']
                            if debug:
                                inkex.utils.debug("reversed seg " +
                                                  str(seg['id']) + ", " +
                                                  str(seg['n']))

                        if self.near_ends(end1, seg['end2']):
                            # prepend seg to chain
                            self.set_segment_done(
                                seg['id'], seg['n'],
                                'prepended to ' + str(id) + ' ' + str(cur_idx))
                            chain = self.link_segments(seg['seg'], chain)
                            end1 = [chain[0][1][0], chain[0][1][1]]
                            segments_idx = 0  # this chain changed. re-visit all candidate
                            continue

                        if self.near_ends(end2, seg['end1']):
                            # append seg to chain
                            self.set_segment_done(
                                seg['id'], seg['n'],
                                'appended to ' + str(id) + ' ' + str(cur_idx))
                            chain = self.link_segments(chain, seg['seg'])
                            end2 = [chain[-1][1][0], chain[-1][1][1]]
                            segments_idx = 0  # this chain changed. re-visit all candidate
                            continue

                        segments_idx += 1

                    # Now all joinable segments are joined.
                    # Finally, we can check, if the resulting path is a closed path:
                    # Closing a path here, isolates it from the rest.
                    # But as we prefer to make the chain as long as possible, we close late.
                    if self.near_ends(
                            end1,
                            end2) and not path_closed and self.close_loops:
                        if debug:
                            inkex.utils.debug("closing closeable loop " +
                                              str(id))
                        if self.snap_ends:
                            # move first point to mid position
                            x1n = (chain[0][1][0] + chain[-1][1][0]) * 0.5
                            y1n = (chain[0][1][1] + chain[-1][1][1]) * 0.5
                            chain[0][1][0], chain[0][1][1] = x1n, y1n
                            # merge handle of the last point to the handle of the first point
                            dx0e = chain[-1][0][0] - chain[-1][1][0]
                            dy0e = chain[-1][0][1] - chain[-1][1][1]
                            if debug:
                                inkex.utils.debug("handle diff: " + str(dx0e) +
                                                  str(dy0e))
                            # FIXME: this does not work. cubicsuperpath.formatPath() ignores this handle.
                            chain[0][0][0], chain[0][0][
                                1] = x1n + dx0e, y1n + dy0e
                            # drop last point
                            chain.pop()
                            end2 = [chain[-1][1][0], chain[-1][1][1]]
                        path_closed = True
                        self.chained_count += 1

                    new.append(chain)

            if not len(new):
                # node.clear()
                if node.getparent() is not None:
                    node.delete()
                    obsoleted += 1
                    if debug:
                        inkex.utils.debug("Path node obsoleted: " + str(id))
            else:
                remaining += 1
                # BUG: All previously closed loops, are open, after we convert them back with cubicsuperpath.formatPath()
                p_fmt = str(Path(CubicSuperPath(new).to_path().to_arrays()))
                if path_closed: p_fmt += " z"
                if debug: inkex.utils.debug("new path: " + str(p_fmt))
                node.set('d', p_fmt)

        # statistics:
        if debug:
            inkex.utils.debug("Path nodes obsoleted: " + str(obsoleted) +
                              "\nPath nodes remaining:" + str(remaining))
        if self.min_missed_distance_sq is not None:
            if debug:
                inkex.utils.debug("min_missed_distance: " + str(
                    math.sqrt(float(self.min_missed_distance_sq)) /
                    self.unit_factor) + '>' + str(self.chain_epsilon) +
                                  str(self.options.units))
        if debug:
            inkex.utils.debug("Successful link operations: " +
                              str(self.chained_count))