diff --git a/extensions/fablabchemnitz/move_path_node/move_path_node.inx b/extensions/fablabchemnitz/move_path_node/move_path_node.inx index f56d616..dd2821b 100644 --- a/extensions/fablabchemnitz/move_path_node/move_path_node.inx +++ b/extensions/fablabchemnitz/move_path_node/move_path_node.inx @@ -17,7 +17,7 @@ - + diff --git a/extensions/fablabchemnitz/move_path_node/move_path_node.py b/extensions/fablabchemnitz/move_path_node/move_path_node.py index 020345e..5c5d0a0 100644 --- a/extensions/fablabchemnitz/move_path_node/move_path_node.py +++ b/extensions/fablabchemnitz/move_path_node/move_path_node.py @@ -1,21 +1,32 @@ #!/usr/bin/env python3 ''' + +This extension changes the order of the nodes without changing the shape of the path. It's required to modify paths +like this for example when projecting text to path or applying bezier envelope transformation. + Author: Mario Voigt / FabLab Chemnitz Mail: mario.voigt@stadtfabrikanten.org Date: 19.05.2021 -Last patch: 19.05.2021 +Last patch: 28.05.2025 License: GNU GPL v3 ''' import copy import inkex +import sys from inkex import Circle, TextElement, PathElement from inkex.paths import CubicSuperPath, Path class MovePathNode(inkex.EffectExtension): def modify(self, element): + + if self.options.debug is True: + inkex.utils.debug("raw root path:") + inkex.utils.debug(element.path) + inkex.utils.debug("-"*25) + raw = element.path.to_arrays() subpaths, prev = [], 0 for i in range(len(raw)): # Breaks compound paths into simple paths @@ -25,9 +36,9 @@ class MovePathNode(inkex.EffectExtension): subpaths.append(raw[prev:]) if self.options.debug is True: if len(subpaths) == 0: - self.msg("{} has no subpaths").format(element.get('id')) + inkex.utils.debug("{} has no subpaths").format(element.get('id')) else: - self.msg("{} has {} subpath(s)".format(element.get('id'), len(subpaths))) + inkex.utils.debug("{} has {} subpath(s)".format(element.get('id'), len(subpaths))) subpathNr = 0 for path in subpaths: @@ -38,18 +49,19 @@ class MovePathNode(inkex.EffectExtension): if path[-1][0] == 'Z' or \ (path[-1][0] == 'L' and path[0][1] == path[-1][1]) or \ (path[-1][0] == 'C' and path[0][1] == [path[-1][1][-2], path[-1][1][-1]]) \ - : #if first is last point the path is also closed. The "Z" command is not required + : #if first is last point the path is also kind of closed, but not cleanly. We assume that matching of start and end targets to be closed pathIsClosed = True if self.options.debug is True: - self.msg("pathIsClosed = " + str(pathIsClosed)) - self.msg("nodes = " + str(len(path))) + nodeCountInitial = len(path) + inkex.utils.debug("pathIsClosed = " + str(pathIsClosed)) + inkex.utils.debug("initial nodes = " + str(nodeCountInitial)) if self.options.closed_only is True and pathIsClosed is False: if len(subpaths) == 0: - self.msg("{}/subpath {} is not closed! Skipping ...".format(element.get('id'), subpathNr)) + inkex.utils.debug("{}/subpath {} is not closed! Skipping ...".format(element.get('id'), subpathNr)) else: - self.msg("{} is not closed! Skipping ...".format(element.get('id'))) + inkex.utils.debug("{} is not closed! Skipping ...".format(element.get('id'))) continue #skip this open path if len(path) == 2: @@ -63,68 +75,91 @@ class MovePathNode(inkex.EffectExtension): moves = (self.options.movenode) % len(path) if pathIsClosed is True: #if closed start and end collapse and "duplicate" moves = (self.options.movenode) % (len(path) - 1) - if self.options.movenode == 0: #special handling for 0 is required + if self.options.movenode == 0: #special handling for 0 is required - means "do nothing" moves = 0 + if moves < 1: + if self.options.debug is True: inkex.utils.debug("Nothing to do (0 moves) ...") + else: + if self.options.debug is True: + inkex.utils.debug("moves to perform = " + str(moves)) + inkex.utils.debug("raw root path:") + inkex.utils.debug(path) + inkex.utils.debug("-"*25) - if self.options.debug is True: - self.msg("moves to perform = " + str(moves)) - self.msg("root path:") - self.msg(path) - self.msg("-"*25) + for i in range(moves): + if len(path) > 2: #the path needs at least more than two segments, else we might just get a "pointy path" on an open path - for i in range(moves): - if len(path) > 2: #the path needs at least more than two segments, else we might just get a "pointy path" on an open path - - #we move the first segment to the end of the list - move = path[0] - del path[0] - path.append(move) - oldseg = copy.deepcopy(path[0]) #if we assign like "oldseg = path[0]", it will get overwritten. So we need copy - - if self.options.debug is True: - self.msg("moved path (move no. {}):".format(i+1)) - self.msg(path) - self.msg("-"*25) - - #Now we messed the integrity of the path. It does not begin with 'M' now. But we need an 'M'. - #It now either starts with L or C. H, V, Z cannot occure here. - if path[0][0] == 'C': #and path[-1][0] == 'M': - #self.msg("C to M") - path[0][1] = [path[0][1][-2], path[0][1][-1]] - elif path[0][0] == 'L': #and path[-1][0] == 'M': - #self.msg("L to M") - path[0][1] = [path[0][1][0], path[0][1][1]] - #else: - # self.msg("no idea") - path[0][0] = 'M' #we really need M. Does not matter if 'L' or 'C'. - - if pathIsClosed is True: - if path[-1][0] == 'M' and len(oldseg[1]) == 2: #data of an 'L' command - path[-1][0] = 'L' - path[-1][1] = path[0][1] - elif path[-1][0] == 'M' and len(oldseg[1]) > 2: #data of an 'C' command - path[-1][0] = 'C' - path[-1][1] = oldseg[1] - else: - if path[-1][0] == 'M': #if open path we just drop the dangling 'M' command completely - del path[-1] + #special case for rare paths: check if first node and last node match and if there is a Z between, which doubles up, while showing 1 node less than exspected + doubleClosed = False + if [path[0][1][-2], path[0][1][-1]] == [path[-2][1][-2], path[-2][1][-1]] and pathIsClosed is True: + doubleClosed = True + if self.options.debug is True: inkex.utils.debug("doubleClosed = " + str(doubleClosed)) + + #we move the first segment to the end of the list + move = path[0] + del path[0] + path.append(move) + oldseg = copy.deepcopy(path[0]) #if we assign like "oldseg = path[0]", it will get overwritten. So we need copy - if self.options.debug is True: - self.msg("final path:") - self.msg(path) - self.msg("-"*25) + if self.options.debug is True: + inkex.utils.debug("moved path (move no. {}):".format(i+1)) + inkex.utils.debug(path) + inkex.utils.debug("-"*25) - newSubpaths[subpathNr - 1] = path - else: - inkex.utils.debug("More moves entered than possible to apply. Path result would be a point, not a line") - #return + #Now we messed the integrity of the path. It does not begin with 'M' now. But we need an 'M'. + #It now either starts with L or C. H, V, Z cannot occure here. + if path[0][0] == 'C': #and path[-1][0] == 'M': + #inkex.utils.debug("C to M") + path[0][1] = [path[0][1][-2], path[0][1][-1]] + elif path[0][0] == 'L': #and path[-1][0] == 'M': + #inkex.utils.debug("L to M") + path[0][1] = [path[0][1][0], path[0][1][1]] + #else: + # inkex.utils.debug("no idea") + + path[0][0] = 'M' #we really need M. Does not matter if 'L' or 'C'. + + if doubleClosed is True: + del path[-2] + + if pathIsClosed is True: + if path[-1][0] == 'M' and len(oldseg[1]) == 2: #data of an 'L' command + path[-1][0] = 'L' + path[-1][1] = path[0][1] + if self.options.debug is True: inkex.utils.debug("modified M command to L command") + elif path[-1][0] == 'M' and len(oldseg[1]) > 2: #data of an 'C' command + path[-1][0] = 'C' + path[-1][1] = oldseg[1] + if self.options.debug is True: inkex.utils.debug("modified M command to C command") + else: + if path[-1][0] == 'M': #if open path we just drop the dangling 'M' command completely + del path[-1] + if self.options.debug is True: inkex.utils.debug("delete dangling M command") + + if pathIsClosed is True and path[-1] != (['Z', []]): + path.append(['Z', []]) + + if self.options.debug is True: + nodeCountFinal = len(path) + inkex.utils.debug("final nodes = " + str(nodeCountFinal)) #that count must match the inital count! + inkex.utils.debug("final path:") + inkex.utils.debug(path) + inkex.utils.debug("-"*25) + if nodeCountInitial != nodeCountFinal: + inkex.utils.debug("Warning! Node count changed from {} to {}".format(nodeCountInitial, nodeCountFinal)) + + newSubpaths[subpathNr - 1] = path + else: + if self.options.debug is True: + inkex.utils.debug("More moves entered than possible to apply. Path result would be a point, not a line") + #return composedPath = inkex.Path() for newSubpath in newSubpaths: composedPath.extend(newSubpath) if self.options.debug is True: - self.msg("Composed path = " + str(composedPath)) + inkex.utils.debug("Composed path = " + str(composedPath)) element.path = composedPath