diff --git a/chain_paths.inx b/chain_paths.inx index 95a49be..7bde072 100644 --- a/chain_paths.inx +++ b/chain_paths.inx @@ -14,10 +14,11 @@ false + true - - + + https://github.com/fablabnbg/inkscape-chain-paths Version 0.6 diff --git a/chain_paths.py b/chain_paths.py index da77d82..3ca17a1 100644 --- a/chain_paths.py +++ b/chain_paths.py @@ -18,22 +18,27 @@ # 2015-11-26 jw, V0.5 -- HACK to resolve some self-reversing path segments. # https://github.com/fablabnbg/inkscape-chain-paths/issues/1 # 2020-04-10 jw, V0.6 -- Close paths correctly. Self reversing path hack was too eager. +# Workaround for cubicsuperpath.parsePath/formatPath limitation. +# Started python3 compatibility. + +from __future__ import print_function __version__ = '0.6' # Keep in sync with chain_paths.inx ca line 22 -__author__ = 'Juergen Weigert ' +__author__ = 'Juergen Weigert ' import sys, os, shutil, time, logging, tempfile, math +import re -debug = True -#debug = False +#debug = True +debug = False # search path, so that inkscape libraries are found when we are standalone. sys_platform = sys.platform.lower() if sys_platform.startswith('win'): # windows sys.path.append('C:\Program Files\Inkscape\share\extensions') -elif sys_platform.startswith('darwin'): # mac +elif sys_platform.startswith('darwin'): # mac sys.path.append('/Applications/Inkscape.app/Contents/Resources/extensions') -else: # linux +else: # linux # if sys_platform.startswith('linux'): sys.path.append('/usr/share/inkscape/extensions') @@ -64,6 +69,7 @@ def __init__(self): # as establishing a transform from the viewbox to the display. self.chain_epsilon = 0.01 self.snap_ends = True + self.close_loops = True self.segments_done = {} self.min_missed_distance_sq = None self.chained_count = 0 @@ -75,18 +81,18 @@ def __init__(self): self.tty = open("CON:", 'w') # windows. Does this work??? except: self.tty = open(os.devnull, 'w') # '/dev/null' for POSIX, 'nul' for Windows. - if debug: print >>self.tty, "__init__" + if debug: print("__init__", file=self.tty) self.OptionParser.add_option('-V', '--version', action = 'store_const', const=True, dest='version', default=False, help = 'Just print version number ("' + __version__ + '") and exit.') self.OptionParser.add_option('-s', '--snap', action='store', dest='snap_ends', type='inkbool', default=True, help='snap end-points together when connecting') + self.OptionParser.add_option('-c', '--close', action='store', dest='close_loops', type='inkbool', default=True, help='close loops (start/end of the same path)') self.OptionParser.add_option('-u', '--units', action='store', dest="units", type="string", default="mm", help="measurement unit for epsilon") self.OptionParser.add_option('-e', '--epsilon', action='store', type='float', dest='chain_epsilon', default=0.01, help="Max. distance to connect [mm]") def version(self): - print "hiuhu" return __version__ def author(self): return __author__ @@ -113,7 +119,7 @@ def set_segment_done(self, id, n, msg=''): if not id in self.segments_done: self.segments_done[id] = {} self.segments_done[id][n] = True - if debug: print >>self.tty, "done", id, n, msg + if debug: print("done", id, n, msg, file=self.tty) def is_segment_done(self, id, n): if not id in self.segments_done: @@ -157,12 +163,13 @@ def near_ends(self, end1, end2): def effect(self): if self.options.version: - print __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 @@ -176,7 +183,7 @@ def effect(self): 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: print >>self.tty, "id=" + str(id), "tag=" + str(node.tag) + if debug: print("id=" + str(id), "tag=" + str(node.tag), file=self.tty) path_d = cubicsuperpath.parsePath(node.get('d')) sub_idx = -1 for sub in path_d: @@ -186,22 +193,22 @@ def effect(self): # [[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: print >>self.tty, " sub=" + str(sub) + if debug: print(" sub=" + str(sub), file=self.tty) end1 = [sub[ 0][1][0], sub[ 0][1][1]] end2 = [sub[-1][1][0], sub[-1][1][1]] - # Removes self revesals when building candidate segments list. + # Remove trivial self revesal when building candidate segments list. if ((len(sub) == 3) and self.near_ends(end1, end2)): - if debug: print >>self.tty, "dropping segment from self-reversing path, length:", len(sub) + if debug: print("dropping segment from self-reversing path, length:", len(sub), file=self.tty) 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: print >>self.tty, "-------- seen:" + if debug: print("-------- seen:", file=self.tty) for s in segments: - if debug: print >>self.tty, s['id'], s['n'], s['end1'], s['end2'] + if debug: print(s['id'], s['n'], s['end1'], s['end2'], file=self.tty) # chain the segments obsoleted = 0 @@ -209,6 +216,8 @@ def effect(self): for id, node in self.selected.iteritems(): # path_style = simplestyle.parseStyle(node.get('style')) path_d = cubicsuperpath.parsePath(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: @@ -225,7 +234,7 @@ def effect(self): end1 = [chain[ 0][1][0], chain[ 0][1][1]] end2 = [chain[-1][1][0], chain[-1][1][1]] - # Removes self revesals when doing the actual chain operations. + # 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]] @@ -241,7 +250,7 @@ def effect(self): self.near_ends(end2, seg['end2'])): seg['seg'] = self.reverse_segment(seg['seg']) seg['end1'], seg['end2'] = seg['end2'], seg['end1'] - if debug: print >>self.tty, "reversed seg", seg['id'], seg['n'] + if debug: print("reversed seg", seg['id'], seg['n'], file=self.tty) if self.near_ends(end1, seg['end2']): # prepend seg to chain @@ -265,22 +274,45 @@ def effect(self): # 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: print("closing closeable loop", id, file=self.tty) + 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: print("handle diff: ", dx0e, dy0e, file=self.tty) + # 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() node.getparent().remove(node) obsoleted += 1 - if debug: print >>self.tty, "Path node obsoleted:", id + if debug: print("Path node obsoleted:", id, file=self.tty) else: remaining += 1 - node.set('d', cubicsuperpath.formatPath(new)) + # BUG: All previously closed loops, are open, after we convert them back with cubicsuperpath.formatPath() + p_fmt = cubicsuperpath.formatPath(new) + if path_closed: p_fmt += " z" + if debug: print("new path :", p_fmt, file=self.tty) + node.set('d', p_fmt) # statistics: - print >>self.tty, "Path nodes obsoleted:", obsoleted, "\nPath nodes remaining:", remaining + if debug: print("Path nodes obsoleted:", obsoleted, "\nPath nodes remaining:", remaining, file=self.tty) if self.min_missed_distance_sq is not None: - print >>self.tty, "min_missed_distance:", math.sqrt(float(self.min_missed_distance_sq))/self.unit_factor, '>', self.chain_epsilon, self.options.units - print >>self.tty, "Successful link operations: ", self.chained_count + if debug: print("min_missed_distance:", math.sqrt(float(self.min_missed_distance_sq))/self.unit_factor, '>', self.chain_epsilon, self.options.units, file=self.tty) + if debug: print("Successful link operations: ", self.chained_count, file=self.tty) if __name__ == '__main__': e = ChainPaths() diff --git a/setup.py b/setup.py index e5dee6b..5b9b3b0 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# +# # sudo zypper in python-setuptools # http://docs.python.org/2/distutils/setupscript.html#installing-additional-files # + +from __future__ import print_function import sys,os,glob,re from distutils.core import setup @@ -13,7 +15,7 @@ e = chain_paths.ChainPaths() m = re.match('(.*)\s+<(.*)>', e.author()) -# print ('.',['Makefile'] + glob.glob('chain_paths*')) +# print('.',['Makefile'] + glob.glob('chain_paths*')) class PyTest(TestCommand): def finalize_options(self): @@ -49,5 +51,5 @@ def run_tests(self): long_description="".join(open('README.md').readlines()), # tests_require=['pytest', 'scipy'], #packages=['pyPdf','reportlab.pdfgen','reportlab.lib.colors','pygame.font' ], -# +# )