Bugfix move path node, extend it

This commit is contained in:
Mario Voigt 2025-05-28 02:28:30 +02:00
parent 3d315df0d6
commit bfebcb246b
2 changed files with 95 additions and 60 deletions

View File

@ -17,7 +17,7 @@
<page name="tab_about" gui-text="About"> <page name="tab_about" gui-text="About">
<label appearance="header">Move Path Node</label> <label appearance="header">Move Path Node</label>
<label>Extension to change starting / end node of a path and visualize it by dots and numbers. You can also use this extension as a trimmer for open paths.</label> <label>Extension to change starting / end node of a path and visualize it by dots and numbers. You can also use this extension as a trimmer for open paths.</label>
<label>2021 - 2023 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label> <label>2021 - 2025 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer /> <spacer />
<label appearance="header">Online Documentation</label> <label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/movepathnode</label> <label appearance="url">https://y.stadtfabrikanten.org/movepathnode</label>

View File

@ -1,21 +1,32 @@
#!/usr/bin/env python3 #!/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 Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org Mail: mario.voigt@stadtfabrikanten.org
Date: 19.05.2021 Date: 19.05.2021
Last patch: 19.05.2021 Last patch: 28.05.2025
License: GNU GPL v3 License: GNU GPL v3
''' '''
import copy import copy
import inkex import inkex
import sys
from inkex import Circle, TextElement, PathElement from inkex import Circle, TextElement, PathElement
from inkex.paths import CubicSuperPath, Path from inkex.paths import CubicSuperPath, Path
class MovePathNode(inkex.EffectExtension): class MovePathNode(inkex.EffectExtension):
def modify(self, element): 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() raw = element.path.to_arrays()
subpaths, prev = [], 0 subpaths, prev = [], 0
for i in range(len(raw)): # Breaks compound paths into simple paths for i in range(len(raw)): # Breaks compound paths into simple paths
@ -25,9 +36,9 @@ class MovePathNode(inkex.EffectExtension):
subpaths.append(raw[prev:]) subpaths.append(raw[prev:])
if self.options.debug is True: if self.options.debug is True:
if len(subpaths) == 0: if len(subpaths) == 0:
self.msg("{} has no subpaths").format(element.get('id')) inkex.utils.debug("{} has no subpaths").format(element.get('id'))
else: 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 subpathNr = 0
for path in subpaths: for path in subpaths:
@ -38,18 +49,19 @@ class MovePathNode(inkex.EffectExtension):
if path[-1][0] == 'Z' or \ if path[-1][0] == 'Z' or \
(path[-1][0] == 'L' and path[0][1] == path[-1][1]) 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]]) \ (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 pathIsClosed = True
if self.options.debug is True: if self.options.debug is True:
self.msg("pathIsClosed = " + str(pathIsClosed)) nodeCountInitial = len(path)
self.msg("nodes = " + str(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 self.options.closed_only is True and pathIsClosed is False:
if len(subpaths) == 0: 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: 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 continue #skip this open path
if len(path) == 2: if len(path) == 2:
@ -63,68 +75,91 @@ class MovePathNode(inkex.EffectExtension):
moves = (self.options.movenode) % len(path) moves = (self.options.movenode) % len(path)
if pathIsClosed is True: #if closed start and end collapse and "duplicate" if pathIsClosed is True: #if closed start and end collapse and "duplicate"
moves = (self.options.movenode) % (len(path) - 1) 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 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: for i in range(moves):
self.msg("moves to perform = " + str(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
self.msg("root path:")
self.msg(path)
self.msg("-"*25)
for i in range(moves): #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
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 doubleClosed = False
if [path[0][1][-2], path[0][1][-1]] == [path[-2][1][-2], path[-2][1][-1]] and pathIsClosed is True:
#we move the first segment to the end of the list doubleClosed = True
move = path[0] if self.options.debug is True: inkex.utils.debug("doubleClosed = " + str(doubleClosed))
del path[0]
path.append(move) #we move the first segment to the end of the list
oldseg = copy.deepcopy(path[0]) #if we assign like "oldseg = path[0]", it will get overwritten. So we need copy move = path[0]
del path[0]
if self.options.debug is True: path.append(move)
self.msg("moved path (move no. {}):".format(i+1)) oldseg = copy.deepcopy(path[0]) #if we assign like "oldseg = path[0]", it will get overwritten. So we need copy
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]
if self.options.debug is True: if self.options.debug is True:
self.msg("final path:") inkex.utils.debug("moved path (move no. {}):".format(i+1))
self.msg(path) inkex.utils.debug(path)
self.msg("-"*25) inkex.utils.debug("-"*25)
newSubpaths[subpathNr - 1] = path #Now we messed the integrity of the path. It does not begin with 'M' now. But we need an 'M'.
else: #It now either starts with L or C. H, V, Z cannot occure here.
inkex.utils.debug("More moves entered than possible to apply. Path result would be a point, not a line") if path[0][0] == 'C': #and path[-1][0] == 'M':
#return #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() composedPath = inkex.Path()
for newSubpath in newSubpaths: for newSubpath in newSubpaths:
composedPath.extend(newSubpath) composedPath.extend(newSubpath)
if self.options.debug is True: if self.options.debug is True:
self.msg("Composed path = " + str(composedPath)) inkex.utils.debug("Composed path = " + str(composedPath))
element.path = composedPath element.path = composedPath