Added several fixes for create_links extension

This commit is contained in:
Mario Voigt 2021-04-13 14:44:28 +02:00
parent 48c9a9630e
commit 18dc01e884
2 changed files with 178 additions and 142 deletions

View File

@ -4,6 +4,11 @@
<id>fablabchemnitz.de.create_links</id> <id>fablabchemnitz.de.create_links</id>
<param name="main_tabs" type="notebook"> <param name="main_tabs" type="notebook">
<page name="settings" gui-text="Settings"> <page name="settings" gui-text="Settings">
<param name="path_types" type="optiongroup" appearance="combo" gui-text="Apply for">
<option value="open_paths">open paths</option>
<option value="closed_paths">closed paths</option>
<option value="both">both</option>
</param>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Units"> <param name="unit" type="optiongroup" appearance="combo" gui-text="Units">
<option value="mm">mm</option> <option value="mm">mm</option>
<option value="cm">cm</option> <option value="cm">cm</option>
@ -14,15 +19,15 @@
<option value="percent">%</option> <option value="percent">%</option>
</param> </param>
<param name="link_count" type="int" min="1" max="9999" gui-text="Link count">1</param> <param name="link_count" type="int" min="1" max="9999" gui-text="Link count">1</param>
<param name="link_multiplicator (experimental)" type="int" min="0" max="9999" gui-text="Link multiplicator" gui-description="If se, we create a set of multiple gaps of same size next to the main gap">0</param>
<param name="length_link" type="float" min="0.000" max="9999.000" precision="3" gui-text="Link length (the length of the gap)">1.000</param> <param name="length_link" type="float" min="0.000" max="9999.000" precision="3" gui-text="Link length (the length of the gap)">1.000</param>
<param name="link_offset" type="float" min="-9999.000.000" max="9999.000" precision="3" gui-text="Link offset (+/-)">0.000</param> <param name="link_multiplicator" type="int" min="0" max="9999" gui-text="Link multiplicator (experimental)" gui-description="If set, we create a set of multiple gaps of same size next to the main gap">0</param>
<param name="link_offset" type="float" min="-9999.000.000" max="9999.000" precision="3" gui-text="Link offset (+/-) (experimental)" gui-description="Does not properly work the intended way if link count is more than 1.">0.000</param>
<param name="length_filter" type="bool" gui-text="Enable path length filtering">false</param> <param name="length_filter" type="bool" gui-text="Enable path length filtering">false</param>
<param name="length_filter_value" type="float" min="0.000" max="9999.000" precision="3" gui-text="Paths with length more than">0.000</param> <param name="length_filter_value" type="float" min="0.000" max="9999.000" precision="3" gui-text="Paths with length more than">0.000</param>
<param name="custom_dasharray" type="bool" gui-text="Enable custom dash pattern">false</param> <param name="custom_dasharray" type="bool" gui-text="Enable custom dash pattern">false</param>
<param name="custom_dasharray_value" type="string" gui-text="Dash pattern" gui-description="A list of separated lengths that specify the lengths of alternating dashes and gaps. Input only accepts numbers. It ignores percentages or other characters.">10 5</param> <param name="custom_dasharray_value" type="string" gui-text="Dash pattern" gui-description="A list of separated lengths that specify the lengths of alternating dashes and gaps. Input only accepts numbers. It ignores percentages or other characters.">10 5</param>
<param name="keep_selected" type="bool" gui-text="Keep selected elements">false</param> <param name="keep_selected" type="bool" gui-text="Keep selected elements">false</param>
<param name="breakapart" type="bool" gui-text="Break apart" gui-description="Performs CTRL + SHIFT + K to break a combined path into it's parts">false</param> <param name="breakapart" type="bool" gui-text="Break apart output paths" gui-description="Performs CTRL + SHIFT + K to break the new output path into it's parts">false</param>
<param name="show_info" type="bool" gui-text="Print some length and pattern information" gui-description="Warning: might freeze InkScape forever if you have a lot of nodes because we create too much print output. Use for debugging only!">false</param> <param name="show_info" type="bool" gui-text="Print some length and pattern information" gui-description="Warning: might freeze InkScape forever if you have a lot of nodes because we create too much print output. Use for debugging only!">false</param>
</page> </page>
<page name="about" gui-text="About"> <page name="about" gui-text="About">

View File

@ -24,6 +24,10 @@ It is a modification of the file addnodes.py
It is a modification of the file convert2dash.py It is a modification of the file convert2dash.py
Extension to convert paths into dash-array line Extension to convert paths into dash-array line
ToDo:
- better handling of dasharray patterns (fix experimental stuff with multiplicator)
- dash offset does not behave the exspected way
- break apart before calculating
""" """
import copy import copy
@ -38,22 +42,23 @@ class LinksCreator(inkex.EffectExtension):
def __init__(self): def __init__(self):
super(LinksCreator, self).__init__() super(LinksCreator, self).__init__()
self.arg_parser.add_argument("--main_tabs") self.arg_parser.add_argument("--main_tabs")
self.arg_parser.add_argument("--path_types", default="closed_paths", help="Apply for closed paths, open paths or both")
self.arg_parser.add_argument("--unit", default="mm", help="Units") self.arg_parser.add_argument("--unit", default="mm", help="Units")
self.arg_parser.add_argument("--link_count", type=int, default=1, help="Link count") self.arg_parser.add_argument("--link_count", type=int, default=1, help="Link count")
self.arg_parser.add_argument("--link_multiplicator", type=int, default=1, help="If se, we create a set of multiple gaps of same size next to the main gap")
self.arg_parser.add_argument("--length_link", type=float, default=1.000, help="Link length") self.arg_parser.add_argument("--length_link", type=float, default=1.000, help="Link length")
self.arg_parser.add_argument("--link_offset", type=float, default=0.000, help="Link offset (+/-)") self.arg_parser.add_argument("--link_multiplicator", type=int, default=1, help="If set, we create a set of multiple gaps of same size next to the main gap")
self.arg_parser.add_argument("--link_offset", type=float, default=0.000, help="Link offset (+/-). Does not properly work the intended way if link count is more than 1.")
self.arg_parser.add_argument("--length_filter", type=inkex.Boolean, default=False, help="Enable path length filtering") self.arg_parser.add_argument("--length_filter", type=inkex.Boolean, default=False, help="Enable path length filtering")
self.arg_parser.add_argument("--length_filter_value", type=float, default=0.000, help="Paths with length more than") self.arg_parser.add_argument("--length_filter_value", type=float, default=0.000, help="Paths with length more than")
self.arg_parser.add_argument("--custom_dasharray", type=inkex.Boolean, default=False, help="Enable custom dash pattern") self.arg_parser.add_argument("--custom_dasharray", type=inkex.Boolean, default=False, help="Enable custom dash pattern")
self.arg_parser.add_argument("--custom_dasharray_value", default="", help="A list of separated lengths that specify the lengths of alternating dashes and gaps. Input only accepts numbers. It ignores percentages or other characters.") self.arg_parser.add_argument("--custom_dasharray_value", default="", help="A list of separated lengths that specify the lengths of alternating dashes and gaps. Input only accepts numbers. It ignores percentages or other characters.")
self.arg_parser.add_argument("--keep_selected", type=inkex.Boolean, default=False, help="Keep selected elements") self.arg_parser.add_argument("--keep_selected", type=inkex.Boolean, default=False, help="Keep selected elements")
self.arg_parser.add_argument("--breakapart", type=inkex.Boolean, default=False, help="Performs CTRL + SHIFT + K to break a combined path into it's parts") self.arg_parser.add_argument("--breakapart", type=inkex.Boolean, default=False, help="Performs CTRL + SHIFT + K to break the new output path into it's parts")
self.arg_parser.add_argument("--show_info", type=inkex.Boolean, default=False, help="Print some length and pattern information") self.arg_parser.add_argument("--show_info", type=inkex.Boolean, default=False, help="Print some length and pattern information")
replacedNodes = [] def breakContours(self, node, breakNodes = None): #this does the same as "CTRL + SHIFT + K"
if breakNodes == None:
def breakContours(self, node): #this does the same as "CTRL + SHIFT + K" breakNodes = []
parent = node.getparent() parent = node.getparent()
idx = parent.index(node) idx = parent.index(node)
idSuffix = 0 idSuffix = 0
@ -67,155 +72,181 @@ class LinksCreator(inkex.EffectExtension):
for subpath in subPaths: for subpath in subPaths:
replacedNode = copy.copy(node) replacedNode = copy.copy(node)
oldId = replacedNode.get('id') oldId = replacedNode.get('id')
replacedNode.set('d', CubicSuperPath(subpath)) replacedNode.set('d', CubicSuperPath(subpath))
replacedNode.set('id', oldId + str(idSuffix).zfill(5)) replacedNode.set('id', oldId + str(idSuffix).zfill(5))
parent.insert(idx, replacedNode) parent.insert(idx, replacedNode)
idSuffix += 1 idSuffix += 1
self.replacedNodes.append(replacedNode) breakNodes.append(replacedNode)
parent.remove(node) parent.remove(node)
for child in node: return breakNodes
self.breakContours(child)
def effect(self): def effect(self):
if len(self.svg.selected) > 0: def createLinks(node):
for node in self.svg.selection.filter(PathElement).values(): nodeParent = node.getparent()
if self.options.keep_selected is True: pathIsClosed = False
parent = node.getparent() path = node.path.to_superpath()
idx = parent.index(node) if path[-1][0] == 'Z' or path[0] == path[-1]: #if first is last point the path is also closed. The "Z" command is not required
copynode = copy.copy(node) pathIsClosed = True
parent.insert(idx, copynode)
# we measure the length of the path to calculate the required dash configuration if self.options.path_types == 'open_paths' and pathIsClosed is True:
csp = node.path.transform(node.composed_transform()).to_superpath() return #skip this loop iteration
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit elif self.options.path_types == 'closed_paths' and pathIsClosed is False:
return #skip this loop iteration
elif self.options.path_types == 'both':
pass
if self.options.keep_selected is True:
parent = node.getparent()
idx = parent.index(node)
copynode = copy.copy(node)
parent.insert(idx, copynode)
# we measure the length of the path to calculate the required dash configuration
csp = node.path.transform(node.composed_transform()).to_superpath()
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
if self.options.unit == "percent":
length_link = (self.options.length_link / 100.0) * stotal
else:
length_link = self.svg.unittouu(str(self.options.length_link) + self.options.unit) length_link = self.svg.unittouu(str(self.options.length_link) + self.options.unit)
if self.options.length_filter is True: if self.options.length_filter is True:
if stotal < self.options.length_filter_value: if stotal < self.options.length_filter_value:
if self.options.show_info is True:
inkex.utils.debug("node " + node.get('id') + " is shorter than minimum allowed length of {:1.3f}. Path length is {:1.3f}".format(self.options.length_filter_value, stotal)) inkex.utils.debug("node " + node.get('id') + " is shorter than minimum allowed length of {:1.3f}. Path length is {:1.3f}".format(self.options.length_filter_value, stotal))
break return #skip this loop iteration
''' '''
<dasharray> <dasharray>
A list of comma and/or white space separated <length>s and <percentage>s that specify the lengths of alternating dashes and gaps. A list of comma and/or white space separated <length>s and <percentage>s that specify the lengths of alternating dashes and gaps.
If an odd number of values is provided, then the list of values is repeated to yield an even number of values. Thus, 5,3,2 is equivalent to 5,3,2,5,3,2. If an odd number of values is provided, then the list of values is repeated to yield an even number of values. Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
If we want three gaps in a path with length of 168.71 mm and a gap length of 2 mm we set the stroke-dasharray to: If we want three gaps in a path with length of 168.71 mm and a gap length of 2 mm we set the stroke-dasharray to:
50.236 2.0 because 3 * 50.236 mm + 3 * 2.0 mm = 168.71 mm 50.236 2.0 because 3 * 50.236 mm + 3 * 2.0 mm = 168.71 mm
examples having a circle with a circumference of length = 100: examples having a circle with a circumference of length = 100:
- the array "20 5" will create 4 dashes with length = 20 and 4 gaps with length = 5 (20 + 5 + 20 + 5 + 20 + 5 + 20 + 5 = 100) - the array "20 5" will create 4 dashes with length = 20 and 4 gaps with length = 5 (20 + 5 + 20 + 5 + 20 + 5 + 20 + 5 = 100)
- the array "5 20" will create 4 dashes with length = 5 and 4 gaps with length = 20 (5 + 20 + 5 + 20 + 5 + 20 + 5 + 20 = 100) - the array "5 20" will create 4 dashes with length = 5 and 4 gaps with length = 20 (5 + 20 + 5 + 20 + 5 + 20 + 5 + 20 = 100)
- the array "5 15" will create 5 dashes with length = 5 and 5 gaps with length = 15 (5 + 15 + 5 + 15 + 5 + 15 + 5 + 15 + 5 + 15 = 100) - the array "5 15" will create 5 dashes with length = 5 and 5 gaps with length = 15 (5 + 15 + 5 + 15 + 5 + 15 + 5 + 15 + 5 + 15 = 100)
- the array "5 14" will create 6 dashes with length = 5 and 5 gaps with length = 15 (5 + 14 + 5 + 14 + 5 + 14 + 5 + 14 + 5 + 14 + 5 = 100) - the first dash will connect to the last dash fluently - the array "5 14" will create 6 dashes with length = 5 and 5 gaps with length = 15 (5 + 14 + 5 + 14 + 5 + 14 + 5 + 14 + 5 + 14 + 5 = 100) - the first dash will connect to the last dash fluently
in the examples above we always match the full length. But we do not always match it. in the examples above we always match the full length. But we do not always match it.
''' '''
dashes = [] dashes = []
dashes.append((stotal - length_link * self.options.link_count) / self.options.link_count) dashes.append((stotal - length_link * self.options.link_count) / self.options.link_count)
dashes.append(length_link) dashes.append(length_link)
for i in range(0, self.options.link_multiplicator): for i in range(0, self.options.link_multiplicator):
dashes.append(length_link) #stroke (=gap) dashes.append(length_link) #stroke (=gap)
dashes.append(length_link) #gap dashes.append(length_link) #gap
if self.options.custom_dasharray is True: if self.options.custom_dasharray is True:
try: try:
floats = [float(dash) for dash in re.findall(r"[-+]?\d*\.\d+|\d+", self.options.custom_dasharray_value)] floats = [float(dash) for dash in re.findall(r"[-+]?\d*\.\d+|\d+", self.options.custom_dasharray_value)]
if len(floats) > 0: if len(floats) > 0:
dashes = floats #overwrite previously calculated values with custom input dashes = floats #overwrite previously calculated values with custom input
else: else:
raise ValueError raise ValueError
except: except:
inkex.errormsg("Error in custom dasharray string (might be empty or does not contain any numbers). Using regular input instead ...") inkex.errormsg("Error in custom dasharray string (might be empty or does not contain any numbers). Using regular input instead ...")
stroke_dasharray = ' '.join(format(dash, "1.3f") for dash in dashes) stroke_dasharray = ' '.join(format(dash, "1.3f") for dash in dashes)
stroke_dashoffset = self.svg.unittouu(str(self.options.link_offset) + self.options.unit) stroke_dashoffset = self.svg.unittouu(str(self.options.link_offset) + self.options.unit)
if self.options.show_info is True: if self.options.show_info is True:
inkex.utils.debug("node " + node.get('id')) inkex.utils.debug("node " + node.get('id'))
inkex.utils.debug("total path length = {:1.3f} {}".format(self.svg.uutounit(stotal, self.options.unit), self.options.unit)) #show length, converted in selected unit if self.options.unit == "percent":
inkex.utils.debug("total gaps = {}".format(self.options.link_count)) inkex.utils.debug("total path length = {:1.3f} {}".format(stotal, self.svg.unit)) #show length, converted in selected unit
inkex.utils.debug("(calculated) dash/gap pattern: {} ({})".format(stroke_dasharray, self.options.unit))
inkex.utils.debug("(calculated) dash offset: {:1.3f} {}".format(self.svg.uutounit(stroke_dashoffset, self.options.unit), self.options.unit))
inkex.utils.debug("----------------------------------------------------------------")
# check if the node has a style attribute. If not we create a blank one with a black stroke and without fill
style = None
if node.attrib.has_key('style'):
style = node.get('style')
if style.endswith(';') is False:
style += ';'
# if has style attribute an dasharray and/or dashoffset are present we modify it accordingly
declarations = style.split(';') # parse the style content and check what we need to adjust
for i, decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-dasharray': #comma separated list of one or more float values
declarations[i] = prop + ':{};'.format(stroke_dasharray)
if prop == 'stroke-dashoffset':
declarations[i] = prop + ':{};'.format(stroke_dashoffset)
node.set('style', ';'.join(declarations)) #apply new style to node
#if has style attribute but the style attribute does not contain dasharray or dashoffset yet
style = node.style
if not 'stroke-dasharray' in style:
style = style + 'stroke-dasharray:{};'.format(stroke_dasharray)
if not 'stroke-dashoffset' in style:
style = style + 'stroke-dashoffset:{};'.format(stroke_dashoffset)
node.set('style', style)
else: else:
style = 'fill:none;stroke:#000000;stroke-width:1px;stroke-dasharray:{};stroke-dashoffset:{};'.format(stroke_dasharray, stroke_dashoffset) inkex.utils.debug("total path length = {:1.3f} {} ({:1.3f} {})".format(self.svg.uutounit(stotal, self.options.unit), self.options.unit, stotal, self.svg.unit)) #show length, converted in selected unit
node.set('style', style) inkex.utils.debug("total gaps = {}".format(self.options.link_count))
inkex.utils.debug("(calculated) gap length: {:1.3f} {}".format(length_link, self.options.unit))
inkex.utils.debug("(calculated) dash/gap pattern: {} ({})".format(stroke_dasharray, self.svg.unit))
inkex.utils.debug("(calculated) dash offset: {:1.3f} {}".format(self.svg.uutounit(stroke_dashoffset, self.options.unit), self.options.unit))
inkex.utils.debug("--------------------------------------------------------------------------------------------------")
style = node.style #get the style again, but this time as style class # check if the node has a style attribute. If not we create a blank one with a black stroke and without fill
style = None
if node.attrib.has_key('style'):
style = node.get('style')
if style.endswith(';') is False:
style += ';'
new = [] # if has style attribute an dasharray and/or dashoffset are present we modify it accordingly
for sub in node.path.to_superpath(): declarations = style.split(';') # parse the style content and check what we need to adjust
idash = 0 for i, decl in enumerate(declarations):
dash = dashes[0] parts = decl.split(':', 2)
length = float(stroke_dashoffset) if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-dasharray': #comma separated list of one or more float values
declarations[i] = prop + ':{};'.format(stroke_dasharray)
if prop == 'stroke-dashoffset':
declarations[i] = prop + ':{};'.format(stroke_dashoffset)
node.set('style', ';'.join(declarations)) #apply new style to node
#if has style attribute but the style attribute does not contain dasharray or dashoffset yet
style = node.style
if not 'stroke-dasharray' in style:
style = style + 'stroke-dasharray:{};'.format(stroke_dasharray)
if not 'stroke-dashoffset' in style:
style = style + 'stroke-dashoffset:{};'.format(stroke_dashoffset)
node.set('style', style)
else:
style = 'fill:none;stroke:#000000;stroke-width:1px;stroke-dasharray:{};stroke-dashoffset:{};'.format(stroke_dasharray, stroke_dashoffset)
node.set('style', style)
style = node.style #get the style again, but this time as style class
new = []
for sub in node.path.to_superpath():
idash = 0
dash = dashes[0]
length = float(stroke_dashoffset)
while dash < length:
length = length - dash
idash = (idash + 1) % len(dashes)
dash = dashes[idash]
new.append([sub[0][:]])
i = 1
while i < len(sub):
dash = dash - length
length = bezier.cspseglength(new[-1][-1], sub[i])
while dash < length: while dash < length:
new[-1][-1], nxt, sub[i] = \
bezier.cspbezsplitatlength(new[-1][-1], sub[i], dash/length)
if idash % 2: # create a gap
new.append([nxt[:]])
else: # splice the curve
new[-1].append(nxt[:])
length = length - dash length = length - dash
idash = (idash + 1) % len(dashes) idash = (idash + 1) % len(dashes)
dash = dashes[idash] dash = dashes[idash]
new.append([sub[0][:]]) if idash % 2:
i = 1 new.append([sub[i]])
while i < len(sub): else:
dash = dash - length new[-1].append(sub[i])
length = bezier.cspseglength(new[-1][-1], sub[i]) i += 1
while dash < length: style.pop('stroke-dasharray')
new[-1][-1], nxt, sub[i] = \ node.pop('sodipodi:type')
bezier.cspbezsplitatlength(new[-1][-1], sub[i], dash/length) node.path = CubicSuperPath(new)
if idash % 2: # create a gap node.style = style
new.append([nxt[:]])
else: # splice the curve
new[-1].append(nxt[:])
length = length - dash
idash = (idash + 1) % len(dashes)
dash = dashes[idash]
if idash % 2:
new.append([sub[i]])
else:
new[-1].append(sub[i])
i += 1
#style.pop('stroke-dasharray')
node.pop('sodipodi:type')
node.path = CubicSuperPath(new)
node.style = style
if self.options.breakapart is True: if self.options.breakapart is True:
self.breakContours(node) breakOutputNodes = self.breakContours(node)
#update selection to split paths segments (does not work, so commented out) breakApartGroup = nodeParent.add(inkex.Group())
#for replacedNode in self.replacedNodes: for breakOutputNode in breakOutputNodes:
# inkex.utils.debug(replacedNode.get('id')) breakApartGroup.append(breakOutputNode)
# self.svg.selection.set(replacedNode.get('id')) #inkex.utils.debug(replacedNode.get('id'))
#self.svg.selection.set(replacedNode.get('id')) #update selection to split paths segments (does not work, so commented out)
if len(self.svg.selected) > 0:
for node in self.svg.selection.filter(PathElement).values():
#at first we need to break down combined nodes to single path, otherwise dasharray cannot properly be applied
breakInputNodes = self.breakContours(node)
for breakInputNode in breakInputNodes:
createLinks(breakInputNode)
else: else:
inkex.errormsg('Please select some objects first.') inkex.errormsg('Please select some objects first.')