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">
<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>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 />
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/movepathnode</label>

View File

@ -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