Upgraded Create Links extension - first well working version

This commit is contained in:
Mario Voigt 2021-04-12 23:28:02 +02:00
parent be4b28db3b
commit 67859849e9
2 changed files with 192 additions and 125 deletions

View File

@ -3,20 +3,7 @@
<name>Create Links (Breakaway Connectors)</name> <name>Create Links (Breakaway Connectors)</name>
<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">
<label appearance="header">Link Parameters</label>
<param name="length_link" type="float" min="0.000" max="9999.000" precision="3" gui-text="Link length">1.000</param>
<param name="length_stroke" type="float" min="0.000" max="9999.000" precision="3" gui-text="Stroke length">0.000</param>
<param name="length_between_strokes" type="float" min="0.000" max="9999.000" precision="3" gui-text="Length between strokes">0.000</param>
<spacer/>
<label appearance="header">Automatic Placement</label>
<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="link_count" type="int" min="0" max="9999" gui-text="Link count">1</param>
<param name="length_between_links" type="float" min="0.000" max="9999.000" precision="3" gui-text="Length between links">100.000</param>
<separator/>
<param name="link_offset" type="float" min="0.000" max="9999.000" precision="3" gui-text="Link offset">0.000</param>
<param name="keep_selected" type="bool" gui-text="Keep selected elements">false</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>
@ -25,14 +12,25 @@
<option value="px">px</option> <option value="px">px</option>
<option value="pc">pc</option> <option value="pc">pc</option>
<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_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="link_offset" type="float" min="-9999.000.000" max="9999.000" precision="3" gui-text="Link offset (+/-)">0.000</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="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="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="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">
<label appearance="header">Create Links</label> <label appearance="header">Create Links</label>
<label>Stadtfabrikanten e.V. (2021)</label> <label>Stadtfabrikanten e.V. (2021)</label>
<spacer/> <spacer/>
<label>This piece of software is part of the MightyScape for InkScape 1.0/1.1dev Extension Collection</label> <label>This piece of software is part of the MightyScape for InkScape 1.0/1.1dev Extension Collection.</label>
<label>you found a bug or got some fresh code? Just report to mario.voigt@stadtfabrikanten.org. Thanks!</label> <label>You found a bug or got some fresh code? Just report to mario.voigt@stadtfabrikanten.org. Thanks!</label>
<label appearance="url">https://fablabchemnitz.de</label> <label appearance="url">https://fablabchemnitz.de</label>
<label>License: GNU GPL v3</label> <label>License: GNU GPL v3</label>
</page> </page>

View File

@ -25,132 +25,201 @@ 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
""" """
import inkex
from inkex import bezier, CubicSuperPath, Group, PathElement
from inkex.bezier import csplength
import copy import copy
import re
import inkex
from inkex import bezier, Path, CubicSuperPath, Group, PathElement
from inkex.bezier import csplength
class LinksCreator(inkex.EffectExtension): 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("--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_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("--length_stroke", type=float, default=0.000, help="Stroke length") self.arg_parser.add_argument("--link_offset", type=float, default=0.000, help="Link offset (+/-)")
self.arg_parser.add_argument("--length_between_strokes", type=float, default=0.000, help="Length between strokes")
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("--link_count", type=int, default=1, help="Link count") self.arg_parser.add_argument("--custom_dasharray", type=inkex.Boolean, default=False, help="Enable custom dash pattern")
self.arg_parser.add_argument("--length_between_links", type=float, default=100.000, help="Length between links") 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("--link_offset", type=float, default=0.000, help="Link offset")
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("--unit", default="mm", help="Units") 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("--show_info", type=inkex.Boolean, default=False, help="Print some length and pattern information")
replacedNodes = []
def breakContours(self, node): #this does the same as "CTRL + SHIFT + K"
parent = node.getparent()
idx = parent.index(node)
idSuffix = 0
raw = Path(node.get("d")).to_arrays()
subPaths, prev = [], 0
for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0:
subPaths.append(raw[prev:i])
prev = i
subPaths.append(raw[prev:])
for subpath in subPaths:
replacedNode = copy.copy(node)
oldId = replacedNode.get('id')
replacedNode.set('d', CubicSuperPath(subpath))
replacedNode.set('id', oldId + str(idSuffix).zfill(5))
parent.insert(idx, replacedNode)
idSuffix += 1
self.replacedNodes.append(replacedNode)
parent.remove(node)
for child in node:
self.breakContours(child)
def effect(self): def effect(self):
for node in self.svg.selection.filter(PathElement).values(): if len(self.svg.selected) > 0:
for node in self.svg.selection.filter(PathElement).values():
if self.options.keep_selected is True:
parent = node.getparent() if self.options.keep_selected is True:
idx = parent.index(node) parent = node.getparent()
copynode = copy.copy(node) idx = parent.index(node)
parent.insert(idx, copynode) 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() # we measure the length of the path to calculate the required dash configuration
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit csp = node.path.transform(node.composed_transform()).to_superpath()
#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 slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
''' length_link = self.svg.unittouu(str(self.options.length_link) + self.options.unit)
<dasharray>
A list of comma and/or white space separated <length>s and <percentage>s that specify the lengths of alternating dashes and gaps. if self.options.length_filter is True:
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 stotal < self.options.length_filter_value:
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))
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: break
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: '''
- 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) <dasharray>
- 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) A list of comma and/or white space separated <length>s and <percentage>s that specify the lengths of alternating dashes and gaps.
- 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) 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.
- 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. 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
#dashes = "{:1.3f} {:1.3f}".format(((stotal / self.options.link_count) - (self.options.length_link * self.options.link_count)), self.options.length_link)
dashes = []
dashes.append((stotal / self.options.link_count) - (self.options.length_link * self.options.link_count))
dashes.append(self.options.length_link)
stroke_dasharray = ' '.join(format(dash, "1.3f") for dash in dashes)
stroke_dashoffset = 0.0
#inkex.utils.debug("dashes = {}".format(stroke_dasharray))
# 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 examples having a circle with a circumference of length = 100:
declarations = style.split(';') # parse the style content and check what we need to adjust - 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)
for i, decl in enumerate(declarations): - 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)
parts = decl.split(':', 2) - 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)
if len(parts) == 2: - 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
(prop, val) = parts in the examples above we always match the full length. But we do not always match it.
prop = prop.strip().lower() '''
if prop == 'stroke-dasharray': #comma separated list of one or more float values
declarations[i] = prop + ':{};'.format(stroke_dasharray) dashes = []
if prop == 'stroke-dashoffset': dashes.append((stotal - length_link * self.options.link_count) / self.options.link_count)
declarations[i] = prop + ':{};'.format(stroke_dashoffset) dashes.append(length_link)
node.set('style', ';'.join(declarations)) #apply new style to node
for i in range(0, self.options.link_multiplicator):
dashes.append(length_link) #stroke (=gap)
dashes.append(length_link) #gap
if self.options.custom_dasharray is True:
try:
floats = [float(dash) for dash in re.findall(r"[-+]?\d*\.\d+|\d+", self.options.custom_dasharray_value)]
if len(floats) > 0:
dashes = floats #overwrite previously calculated values with custom input
else:
raise ValueError
except:
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_dashoffset = self.svg.unittouu(str(self.options.link_offset) + self.options.unit)
if self.options.show_info is True:
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
inkex.utils.debug("total gaps = {}".format(self.options.link_count))
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:
style = 'fill:none;stroke:#000000;stroke-width:1px;stroke-dasharray:{};stroke-dashoffset:{};'.format(stroke_dasharray, stroke_dashoffset)
node.set('style', style)
#if has style attribute but the style attribute does not contain dasharray or dashoffset yet style = node.style #get the style again, but this time as style class
style = node.style
if not 'stroke-dasharray' in style: new = []
style = style + 'stroke-dasharray:{};'.format(stroke_dasharray) for sub in node.path.to_superpath():
if not 'stroke-dashoffset' in style: idash = 0
style = style + 'stroke-dashoffset:{};'.format(stroke_dashoffset) dash = dashes[0]
node.set('style', style) length = float(stroke_dashoffset)
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]
if idash % 2: new.append([sub[0][:]])
new.append([sub[i]]) i = 1
else: while i < len(sub):
new[-1].append(sub[i]) dash = dash - length
i += 1 length = bezier.cspseglength(new[-1][-1], sub[i])
#style.pop('stroke-dasharray') while dash < length:
node.pop('sodipodi:type') new[-1][-1], nxt, sub[i] = \
node.path = CubicSuperPath(new) bezier.cspbezsplitatlength(new[-1][-1], sub[i], dash/length)
node.style = style if idash % 2: # create a gap
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:
self.breakContours(node)
#update selection to split paths segments (does not work, so commented out)
#for replacedNode in self.replacedNodes:
# inkex.utils.debug(replacedNode.get('id'))
# self.svg.selection.set(replacedNode.get('id'))
else:
inkex.errormsg('Please select some objects first.')
return
if __name__ == '__main__': if __name__ == '__main__':
LinksCreator().run() LinksCreator().run()