|
|
@ -1,275 +0,0 @@ |
|
|
|
#!/usr/bin/env python |
|
|
|
# |
|
|
|
# Inkscape extension making long continuous paths from shorter pieces. |
|
|
|
# (C) 2015 juewei@fabmail.org |
|
|
|
# |
|
|
|
# code snippets visited to learn the extension 'effect' interface: |
|
|
|
# - convert2dashes.py |
|
|
|
# - http://github.com/jnweiger/inkscape-silhouette |
|
|
|
# - http://github.com/jnweiger/inkscape-gears-dev |
|
|
|
# - http://sourceforge.net/projects/inkcut/ |
|
|
|
# - http://code.google.com/p/inkscape2tikz/ |
|
|
|
# - http://code.google.com/p/eggbotcode/ |
|
|
|
# |
|
|
|
# 2015-11-30 jw, V0.1 -- initial draught |
|
|
|
|
|
|
|
__version__ = '0.1' # Keep in sync with clean_path.inx ca line 22 |
|
|
|
__author__ = 'Juergen Weigert <juergen@fabmail.org>' |
|
|
|
|
|
|
|
import sys, os, shutil, time, logging, tempfile, math |
|
|
|
|
|
|
|
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 |
|
|
|
sys.path.append('/Applications/Inkscape.app/Contents/Resources/extensions') |
|
|
|
else: # linux |
|
|
|
# if sys_platform.startswith('linux'): |
|
|
|
sys.path.append('/usr/share/inkscape/extensions') |
|
|
|
|
|
|
|
# inkscape libraries |
|
|
|
import inkex |
|
|
|
import cubicsuperpath |
|
|
|
|
|
|
|
inkex.localize() |
|
|
|
|
|
|
|
from optparse import SUPPRESS_HELP |
|
|
|
|
|
|
|
def uutounit(self,nn,uu): |
|
|
|
try: |
|
|
|
return self.uutounit(nn,uu) # inkscape 0.91 |
|
|
|
except: |
|
|
|
return inkex.uutounit(nn,uu) # inkscape 0.48 |
|
|
|
|
|
|
|
class ChainPaths(inkex.Effect): |
|
|
|
""" |
|
|
|
Inkscape Extension make long continuous paths from smaller parts |
|
|
|
""" |
|
|
|
def __init__(self): |
|
|
|
# Call the base class constructor. |
|
|
|
inkex.Effect.__init__(self) |
|
|
|
|
|
|
|
# For handling an SVG viewbox attribute, we will need to know the |
|
|
|
# values of the document's <svg> width and height attributes as well |
|
|
|
# as establishing a transform from the viewbox to the display. |
|
|
|
self.chain_epsilon = 0.01 |
|
|
|
self.snap_ends = True |
|
|
|
self.segments_done = {} |
|
|
|
self.min_missed_distance_sq = None |
|
|
|
self.chained_count = 0 |
|
|
|
|
|
|
|
try: |
|
|
|
self.tty = open("/dev/tty", 'w') |
|
|
|
except: |
|
|
|
try: |
|
|
|
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__" |
|
|
|
|
|
|
|
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('-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__ |
|
|
|
|
|
|
|
def calc_unit_factor(self, units='mm'): |
|
|
|
""" return the scale factor for all dimension conversions. |
|
|
|
- The document units are always irrelevant as |
|
|
|
everything in inkscape is expected to be in 90dpi pixel units |
|
|
|
""" |
|
|
|
dialog_units = uutounit(self, 1.0, units) |
|
|
|
self.unit_factor = 1.0 / dialog_units |
|
|
|
return self.unit_factor |
|
|
|
|
|
|
|
def reverse_segment(self, seg): |
|
|
|
r = [] |
|
|
|
for s in reversed(seg): |
|
|
|
# s has 3 elements: handle1, point, handle2 |
|
|
|
# Swap handles. |
|
|
|
s.reverse() |
|
|
|
r.append(s) |
|
|
|
return r |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
def is_segment_done(self, id, n): |
|
|
|
if not id in self.segments_done: |
|
|
|
return False |
|
|
|
if n in self.segments_done[id]: |
|
|
|
return True |
|
|
|
return False |
|
|
|
|
|
|
|
def link_segments(self, seg1, seg2): |
|
|
|
if self.snap_ends: |
|
|
|
seg = seg1[:-1] |
|
|
|
p1 = seg1[-1] |
|
|
|
p2 = seg2[0] |
|
|
|
# fuse p1 and p2 to create one new point: |
|
|
|
# first handle from p1, point coordinates averaged, second handle from p2 |
|
|
|
seg.append([ [ p1[0][0] , p1[0][1] ], |
|
|
|
[(p1[1][0]+p2[1][0])*.5, (p1[1][1]+p2[1][1])*.5], |
|
|
|
[ p2[2][0] , p2[2][1] ] ]) |
|
|
|
seg.extend(seg2[1:]) |
|
|
|
else: |
|
|
|
seg = seg1[:] |
|
|
|
seg.extend(seg2[:]) |
|
|
|
self.chained_count += 1 |
|
|
|
return seg |
|
|
|
|
|
|
|
|
|
|
|
def near_ends(self, end1, end2): |
|
|
|
""" requires self.eps_sq to be the square of the near distance """ |
|
|
|
dx = end1[0] - end2[0] |
|
|
|
dy = end1[1] - end2[1] |
|
|
|
d_sq = dx * dx + dy * dy |
|
|
|
if d_sq > self.eps_sq: |
|
|
|
if self.min_missed_distance_sq is None: |
|
|
|
self.min_missed_distance_sq = d_sq |
|
|
|
elif self.min_missed_distance_sq > d_sq: |
|
|
|
self.min_missed_distance_sq = d_sq |
|
|
|
return False |
|
|
|
else: |
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
def effect(self): |
|
|
|
if self.options.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.chain_epsilon is not None: self.chain_epsilon = self.options.chain_epsilon |
|
|
|
if self.chain_epsilon < 0.0001: self.chain_epsilon = 0.0001 # keep a minimum. |
|
|
|
self.eps_sq = self.chain_epsilon * self.unit_factor * self.chain_epsilon * self.unit_factor |
|
|
|
|
|
|
|
if not len(self.selected.items()): |
|
|
|
inkex.errormsg(_("Please select one or more objects.")) |
|
|
|
return |
|
|
|
|
|
|
|
segments = [] |
|
|
|
for id, node in self.selected.iteritems(): |
|
|
|
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) |
|
|
|
path_d = cubicsuperpath.parsePath(node.get('d')) |
|
|
|
sub_idx = -1 |
|
|
|
for sub in path_d: |
|
|
|
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]]] |
|
|
|
# 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]] |
|
|
|
if debug: print >>self.tty, " sub="+str(sub) |
|
|
|
end1=[sub[0][1][0],sub[0][1][1]] |
|
|
|
end2=[sub[-1][1][0],sub[-1][1][1]] |
|
|
|
|
|
|
|
while ((len(sub) > 1) and self.near_ends(end1, end2)): |
|
|
|
# FIXME: this also splits any closed path. eg. a rectangle. |
|
|
|
if debug: print >>self.tty, "splitting self-reversing path, length:", len(sub) |
|
|
|
## We split the path and generate more snippets. |
|
|
|
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() |
|
|
|
end2=splitp |
|
|
|
|
|
|
|
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:" |
|
|
|
for s in segments: |
|
|
|
if debug: print >>self.tty, s['id'],s['n'],s['end1'],s['end2'] |
|
|
|
|
|
|
|
# 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')) |
|
|
|
new=[] |
|
|
|
cur_idx = -1 |
|
|
|
for cur in path_d: |
|
|
|
cur_idx += 1 |
|
|
|
if not self.is_segment_done(id, cur_idx): |
|
|
|
# quadratic algorithm: we check both ends of the current segment. |
|
|
|
# If one of them is near another known end from the segments list, we |
|
|
|
# chain this segment to the current segment and remove it from the |
|
|
|
# list, |
|
|
|
# end1-end1 or end2-end2: The new segment is reversed. |
|
|
|
# end1-end2: The new segment is prepended 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. |
|
|
|
end1=[cur[0][1][0],cur[0][1][1]] |
|
|
|
end2=[cur[-1][1][0],cur[-1][1][1]] |
|
|
|
segments_idx = 0 |
|
|
|
while segments_idx < len(segments): |
|
|
|
seg = segments[segments_idx] |
|
|
|
if self.is_segment_done(seg['id'], seg['n']): |
|
|
|
segments_idx += 1 |
|
|
|
continue |
|
|
|
|
|
|
|
if (self.near_ends(end1, seg['end1']) or |
|
|
|
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 self.near_ends(end1, seg['end2']): |
|
|
|
# prepend seg to cur |
|
|
|
self.set_segment_done(seg['id'], seg['n'], 'prepended to '+id+' '+str(cur_idx)) |
|
|
|
cur = self.link_segments(seg['seg'], cur) |
|
|
|
end1=[cur[0][1][0],cur[0][1][1]] |
|
|
|
segments_idx = 0 |
|
|
|
continue |
|
|
|
|
|
|
|
if self.near_ends(end2, seg['end1']): |
|
|
|
# append seg to cur |
|
|
|
self.set_segment_done(seg['id'], seg['n'], 'appended to '+id+' '+str(cur_idx)) |
|
|
|
cur = self.link_segments(cur, seg['seg']) |
|
|
|
end2=[cur[-1][1][0],cur[-1][1][1]] |
|
|
|
segments_idx = 0 |
|
|
|
continue |
|
|
|
|
|
|
|
segments_idx += 1 |
|
|
|
|
|
|
|
new.append(cur) |
|
|
|
|
|
|
|
if not len(new): |
|
|
|
# node.clear() |
|
|
|
node.getparent().remove(node) |
|
|
|
obsoleted += 1 |
|
|
|
if debug: print >>self.tty, "Path node obsoleted:", id |
|
|
|
else: |
|
|
|
remaining += 1 |
|
|
|
node.set('d',cubicsuperpath.formatPath(new)) |
|
|
|
|
|
|
|
# statistics: |
|
|
|
print >>self.tty, "Path nodes obsoleted:", obsoleted, "\nPath nodes remaining:", remaining |
|
|
|
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 __name__ == '__main__': |
|
|
|
e = ChainPaths() |
|
|
|
|
|
|
|
e.affect() |
|
|
|
sys.exit(0) # helps to keep the selection |