Skip to content

Commit

Permalink
loop closing with snap implemented.
Browse files Browse the repository at this point in the history
No workaround for buggy cubicsuperpath.formatPath() found.
  • Loading branch information
jnweiger committed Apr 20, 2020
1 parent a6687cd commit bf67c31
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 27 deletions.
5 changes: 3 additions & 2 deletions chain_paths.inx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
<option value="pt">pt</option>
<option value="px">px</option></param>
<param name="snap" type="boolean" _gui-text="Snap connecting ends together">false</param>
<param name="close" type="boolean" _gui-text="Close loops (start/end of the same path)">true</param>
<!-- Keep in sync with chain_paths.py line 19 __version__ = ... -->
<param name="about_version" type="description">


https://github.com/fablabnbg/inkscape-chain-paths
Version 0.6</param>

Expand Down
76 changes: 54 additions & 22 deletions chain_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <juewei@fabmail.org>'
__author__ = 'Juergen Weigert <juergen@fabmail.org>'

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

Expand Down Expand Up @@ -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
Expand All @@ -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__
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -186,29 +193,31 @@ 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
remaining = 0
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:
Expand All @@ -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]]
Expand All @@ -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
Expand All @@ -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()
Expand Down
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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' ],
#
#
)

0 comments on commit bf67c31

Please sign in to comment.