Updated chain paths extension to 0.6 release; added missing STLConverter executable
This commit is contained in:
parent
404e0b0462
commit
d0752fbcef
@ -14,12 +14,13 @@
|
|||||||
<option value="px">px</option>
|
<option value="px">px</option>
|
||||||
</param>
|
</param>
|
||||||
<param name="snap" type="boolean" _gui-text="Snap connecting ends together">false</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__ = ... -->
|
<!-- Keep in sync with chain_paths.py line 19 __version__ = ... -->
|
||||||
<param name="about_version" type="description">
|
<param name="about_version" type="description">
|
||||||
|
|
||||||
|
|
||||||
https://github.com/fablabnbg/inkscape-chain-paths
|
https://github.com/fablabnbg/inkscape-chain-paths
|
||||||
Version 0.5</param>
|
Version 0.6</param>
|
||||||
<effect needs-live-preview="false">
|
<effect needs-live-preview="false">
|
||||||
<object-type>path</object-type>
|
<object-type>path</object-type>
|
||||||
<effects-menu>
|
<effects-menu>
|
||||||
|
@ -17,11 +17,17 @@
|
|||||||
# 2015-11-16 jw, V0.4 -- gui fully functional.
|
# 2015-11-16 jw, V0.4 -- gui fully functional.
|
||||||
# 2015-11-26 jw, V0.5 -- HACK to resolve some self-reversing path segments.
|
# 2015-11-26 jw, V0.5 -- HACK to resolve some self-reversing path segments.
|
||||||
# https://github.com/fablabnbg/inkscape-chain-paths/issues/1
|
# 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.
|
||||||
|
|
||||||
__version__ = '0.5' # Keep in sync with chain_paths.inx ca line 22
|
from __future__ import print_function
|
||||||
__author__ = 'Juergen Weigert <juewei@fabmail.org>'
|
|
||||||
|
__version__ = '0.6' # Keep in sync with chain_paths.inx ca line 22
|
||||||
|
__author__ = 'Juergen Weigert <juergen@fabmail.org>'
|
||||||
|
|
||||||
import sys, os, shutil, time, logging, tempfile, math
|
import sys, os, shutil, time, logging, tempfile, math
|
||||||
|
import re
|
||||||
|
|
||||||
#debug = True
|
#debug = True
|
||||||
debug = False
|
debug = False
|
||||||
@ -63,6 +69,7 @@ class ChainPaths(inkex.Effect):
|
|||||||
# as establishing a transform from the viewbox to the display.
|
# as establishing a transform from the viewbox to the display.
|
||||||
self.chain_epsilon = 0.01
|
self.chain_epsilon = 0.01
|
||||||
self.snap_ends = True
|
self.snap_ends = True
|
||||||
|
self.close_loops = True
|
||||||
self.segments_done = {}
|
self.segments_done = {}
|
||||||
self.min_missed_distance_sq = None
|
self.min_missed_distance_sq = None
|
||||||
self.chained_count = 0
|
self.chained_count = 0
|
||||||
@ -74,18 +81,18 @@ class ChainPaths(inkex.Effect):
|
|||||||
self.tty = open("CON:", 'w') # windows. Does this work???
|
self.tty = open("CON:", 'w') # windows. Does this work???
|
||||||
except:
|
except:
|
||||||
self.tty = open(os.devnull, 'w') # '/dev/null' for POSIX, 'nul' for Windows.
|
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',
|
self.OptionParser.add_option('-V', '--version',
|
||||||
action = 'store_const', const=True, dest='version', default=False,
|
action = 'store_const', const=True, dest='version', default=False,
|
||||||
help = 'Just print version number ("' + __version__ + '") and exit.')
|
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('-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('-u', '--units', action='store', dest="units", type="string", default="mm", help="measurement unit for epsilon")
|
||||||
self.OptionParser.add_option('-e', '--epsilon', action='store',
|
self.OptionParser.add_option('-e', '--epsilon', action='store',
|
||||||
type='float', dest='chain_epsilon', default=0.01, help="Max. distance to connect [mm]")
|
type='float', dest='chain_epsilon', default=0.01, help="Max. distance to connect [mm]")
|
||||||
|
|
||||||
def version(self):
|
def version(self):
|
||||||
print "hiuhu"
|
|
||||||
return __version__
|
return __version__
|
||||||
def author(self):
|
def author(self):
|
||||||
return __author__
|
return __author__
|
||||||
@ -112,7 +119,7 @@ class ChainPaths(inkex.Effect):
|
|||||||
if not id in self.segments_done:
|
if not id in self.segments_done:
|
||||||
self.segments_done[id] = {}
|
self.segments_done[id] = {}
|
||||||
self.segments_done[id][n] = True
|
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):
|
def is_segment_done(self, id, n):
|
||||||
if not id in self.segments_done:
|
if not id in self.segments_done:
|
||||||
@ -156,12 +163,13 @@ class ChainPaths(inkex.Effect):
|
|||||||
|
|
||||||
def effect(self):
|
def effect(self):
|
||||||
if self.options.version:
|
if self.options.version:
|
||||||
print __version__
|
print(__version__)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
self.calc_unit_factor(self.options.units)
|
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.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.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.
|
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
|
self.eps_sq = self.chain_epsilon * self.unit_factor * self.chain_epsilon * self.unit_factor
|
||||||
@ -175,33 +183,32 @@ class ChainPaths(inkex.Effect):
|
|||||||
if node.tag != inkex.addNS('path', 'svg'):
|
if node.tag != inkex.addNS('path', 'svg'):
|
||||||
inkex.errormsg(_("Object " + id + " is not a path. Try\n - Path->Object to Path\n - Object->Ungroup"))
|
inkex.errormsg(_("Object " + id + " is not a path. Try\n - Path->Object to Path\n - Object->Ungroup"))
|
||||||
return
|
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'))
|
path_d = cubicsuperpath.parsePath(node.get('d'))
|
||||||
sub_idx = -1
|
sub_idx = -1
|
||||||
for sub in path_d:
|
for sub in path_d:
|
||||||
sub_idx += 1
|
sub_idx += 1
|
||||||
# sub = [[[200.0, 300.0], [200.0, 300.0], [175.0, 290.0]], [[175.0, 265.0], [220.37694, 256.99876], [175.0, 240.0]], [[175.0, 215.0], [200.0, 200.0], [200.0, 200.0]]]
|
# sub = [[[200.0, 300.0], [200.0, 300.0], [175.0, 290.0]], [[175.0, 265.0], [220.37694, 256.99876], [175.0, 240.0]], [[175.0, 215.0], [200.0, 200.0], [200.0, 200.0]]]
|
||||||
# this is a path of three points. All the bezier handles are included. the Structure is:
|
# this is a path of three points. All the bezier handles are included. the Structure is:
|
||||||
# [[handle0_x, point0, handle0_1], [handle1_0, point1, handle1_2], [handle2_1, point2, handle2_x]]
|
# [[handle0_OUT, point0, handle0_1], [handle1_0, point1, handle1_2], [handle2_1, point2, handle2_OUT]]
|
||||||
if debug: print >>self.tty, " sub="+str(sub)
|
# the _OUT handles at the end of the path are ignored. The data structure has them identical to their points.
|
||||||
|
#
|
||||||
|
if debug: print(" sub=" + str(sub), file=self.tty)
|
||||||
end1 = [sub[ 0][1][0], sub[ 0][1][1]]
|
end1 = [sub[ 0][1][0], sub[ 0][1][1]]
|
||||||
end2 = [sub[-1][1][0], sub[-1][1][1]]
|
end2 = [sub[-1][1][0], sub[-1][1][1]]
|
||||||
|
|
||||||
while ((len(sub) > 1) and self.near_ends(end1, end2)):
|
# Remove trivial self revesal when building candidate segments list.
|
||||||
if debug: print >>self.tty, "splitting self-reversing path, length:", len(sub)
|
if ((len(sub) == 3) and self.near_ends(end1, end2)):
|
||||||
## We split the path and generate more snippets.
|
if debug: print("dropping segment from self-reversing path, length:", len(sub), file=self.tty)
|
||||||
splitp=[sub[-2][1][0],sub[-2][1][1]]
|
|
||||||
segments.append({'id': id, 'n': sub_idx, 'end1': splitp, 'end2':end2, 'seg': [sub[-2],sub[-1]]})
|
|
||||||
sub_idx += 1
|
|
||||||
sub.pop()
|
sub.pop()
|
||||||
end2=splitp
|
end2 = [sub[-1][1][0], sub[-1][1][1]]
|
||||||
|
|
||||||
segments.append({'id': id, 'n': sub_idx, 'end1': end1, 'end2':end2, 'seg': sub})
|
segments.append({'id': id, 'n': sub_idx, 'end1': end1, 'end2':end2, 'seg': sub})
|
||||||
if node.get(inkex.addNS('type', 'sodipodi')):
|
if node.get(inkex.addNS('type', 'sodipodi')):
|
||||||
del node.attrib[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:
|
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
|
# chain the segments
|
||||||
obsoleted = 0
|
obsoleted = 0
|
||||||
@ -209,9 +216,11 @@ class ChainPaths(inkex.Effect):
|
|||||||
for id, node in self.selected.iteritems():
|
for id, node in self.selected.iteritems():
|
||||||
# path_style = simplestyle.parseStyle(node.get('style'))
|
# path_style = simplestyle.parseStyle(node.get('style'))
|
||||||
path_d = cubicsuperpath.parsePath(node.get('d'))
|
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 = []
|
new = []
|
||||||
cur_idx = -1
|
cur_idx = -1
|
||||||
for cur in path_d:
|
for chain in path_d:
|
||||||
cur_idx += 1
|
cur_idx += 1
|
||||||
if not self.is_segment_done(id, cur_idx):
|
if not self.is_segment_done(id, cur_idx):
|
||||||
# quadratic algorithm: we check both ends of the current segment.
|
# quadratic algorithm: we check both ends of the current segment.
|
||||||
@ -222,8 +231,14 @@ class ChainPaths(inkex.Effect):
|
|||||||
# end1-end2: The new segment is prepended to the current segment.
|
# end1-end2: The new segment is prepended to the current segment.
|
||||||
# end2-end1: The new segment is appended to the current segment.
|
# end2-end1: The new segment is appended to the current segment.
|
||||||
self.set_segment_done(id, cur_idx, "output") # do not cross with ourselves.
|
self.set_segment_done(id, cur_idx, "output") # do not cross with ourselves.
|
||||||
end1=[cur[0][1][0],cur[0][1][1]]
|
end1 = [chain[ 0][1][0], chain[ 0][1][1]]
|
||||||
end2=[cur[-1][1][0],cur[-1][1][1]]
|
end2 = [chain[-1][1][0], chain[-1][1][1]]
|
||||||
|
|
||||||
|
# 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]]
|
||||||
|
|
||||||
segments_idx = 0
|
segments_idx = 0
|
||||||
while segments_idx < len(segments):
|
while segments_idx < len(segments):
|
||||||
seg = segments[segments_idx]
|
seg = segments[segments_idx]
|
||||||
@ -235,42 +250,69 @@ class ChainPaths(inkex.Effect):
|
|||||||
self.near_ends(end2, seg['end2'])):
|
self.near_ends(end2, seg['end2'])):
|
||||||
seg['seg'] = self.reverse_segment(seg['seg'])
|
seg['seg'] = self.reverse_segment(seg['seg'])
|
||||||
seg['end1'], seg['end2'] = seg['end2'], seg['end1']
|
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']):
|
if self.near_ends(end1, seg['end2']):
|
||||||
# prepend seg to cur
|
# prepend seg to chain
|
||||||
self.set_segment_done(seg['id'], seg['n'], 'prepended to ' + id + ' ' + str(cur_idx))
|
self.set_segment_done(seg['id'], seg['n'], 'prepended to ' + id + ' ' + str(cur_idx))
|
||||||
cur = self.link_segments(seg['seg'], cur)
|
chain = self.link_segments(seg['seg'], chain)
|
||||||
end1=[cur[0][1][0],cur[0][1][1]]
|
end1 = [chain[0][1][0], chain[0][1][1]]
|
||||||
segments_idx = 0
|
segments_idx = 0 # this chain changed. re-visit all candidate
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.near_ends(end2, seg['end1']):
|
if self.near_ends(end2, seg['end1']):
|
||||||
# append seg to cur
|
# append seg to chain
|
||||||
self.set_segment_done(seg['id'], seg['n'], 'appended to ' + id + ' ' + str(cur_idx))
|
self.set_segment_done(seg['id'], seg['n'], 'appended to ' + id + ' ' + str(cur_idx))
|
||||||
cur = self.link_segments(cur, seg['seg'])
|
chain = self.link_segments(chain, seg['seg'])
|
||||||
end2=[cur[-1][1][0],cur[-1][1][1]]
|
end2 = [chain[-1][1][0], chain[-1][1][1]]
|
||||||
segments_idx = 0
|
segments_idx = 0 # this chain changed. re-visit all candidate
|
||||||
continue
|
continue
|
||||||
|
|
||||||
segments_idx += 1
|
segments_idx += 1
|
||||||
|
|
||||||
new.append(cur)
|
# Now all joinable segments are joined.
|
||||||
|
# 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):
|
if not len(new):
|
||||||
# node.clear()
|
# node.clear()
|
||||||
node.getparent().remove(node)
|
node.getparent().remove(node)
|
||||||
obsoleted += 1
|
obsoleted += 1
|
||||||
if debug: print >>self.tty, "Path node obsoleted:", id
|
if debug: print("Path node obsoleted:", id, file=self.tty)
|
||||||
else:
|
else:
|
||||||
remaining += 1
|
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:
|
# 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:
|
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
|
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)
|
||||||
print >>self.tty, "Successful link operations: ", self.chained_count
|
if debug: print("Successful link operations: ", self.chained_count, file=self.tty)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
e = ChainPaths()
|
e = ChainPaths()
|
||||||
|
BIN
extensions/papercraft/STLConverter.exe
Normal file
BIN
extensions/papercraft/STLConverter.exe
Normal file
Binary file not shown.
BIN
extensions/svg2shenzhen/.DS_Store
vendored
BIN
extensions/svg2shenzhen/.DS_Store
vendored
Binary file not shown.
Reference in New Issue
Block a user