300 lines
18 KiB
Python
300 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (C) 2005,2007 Aaron Spike, aaron@ekips.org
|
|
# Copyright (C) 2009 Alvin Penner, penner@vaxxine.com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
"""
|
|
This extension converts a path into a dashed line using 'stroke-dasharray'
|
|
It is a modification of the file addelements.py
|
|
It is a modification of the file convert2dash.py
|
|
Extension to convert paths into dash-array line
|
|
|
|
Extension for InkScape 1.X
|
|
Author: Mario Voigt / FabLab Chemnitz
|
|
Mail: mario.voigt@stadtfabrikanten.org
|
|
Date: 09.04.2021
|
|
Last patch: 28.10.2021
|
|
License: GNU GPL v3
|
|
"""
|
|
|
|
import copy
|
|
import re
|
|
|
|
import inkex
|
|
from inkex import bezier, CubicSuperPath, Group, PathElement
|
|
from inkex.bezier import csplength
|
|
|
|
|
|
class LinksCreator(inkex.EffectExtension):
|
|
|
|
def add_arguments(self, pars):
|
|
pars.add_argument("--tab")
|
|
pars.add_argument("--path_types", default="closed_paths", help="Apply for closed paths, open paths or both")
|
|
pars.add_argument("--creationunit", default="mm", help="Creation Units")
|
|
pars.add_argument("--creationtype", default="entered_values", help="Creation")
|
|
pars.add_argument("--link_count", type=int, default=1, help="Link count")
|
|
pars.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")
|
|
pars.add_argument("--length_link", type=float, default=1.000, help="Link length")
|
|
pars.add_argument("--link_offset", type=float, default=0.000, help="Link offset (+/-)")
|
|
pars.add_argument("--switch_pattern", type=inkex.Boolean, default=False, help="If enabled, we use gap length as dash length (switches the dasharray pattern")
|
|
pars.add_argument("--weakening_mode", type=inkex.Boolean, default=False, help="If enabled, we colorize the swap links in #0000ff (blue) and disable the option 'Keep selected elements'")
|
|
pars.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.")
|
|
pars.add_argument("--custom_dashoffset_value", type=float, default=0.000, help="Link offset (+/-)")
|
|
pars.add_argument("--length_filter", type=inkex.Boolean, default=False, help="Enable path length filtering")
|
|
pars.add_argument("--length_filter_value", type=float, default=0.000, help="Paths with length more than")
|
|
pars.add_argument("--length_filter_unit", default="mm", help="Length filter unit")
|
|
pars.add_argument("--keep_selected", type=inkex.Boolean, default=False, help="Keep selected elements")
|
|
pars.add_argument("--no_convert", type=inkex.Boolean, default=False, help="Do not create segments (cosmetic gaps only)")
|
|
pars.add_argument("--breakapart", type=inkex.Boolean, default=True, help="Performs CTRL + SHIFT + K to break the new output path into it's parts. Recommended to enable because default break apart of Inkscape might produce pointy paths.")
|
|
pars.add_argument("--show_info", type=inkex.Boolean, default=False, help="Print some length and pattern information")
|
|
pars.add_argument("--skip_errors", type=inkex.Boolean, default=False, help="Skip errors")
|
|
|
|
def breakContours(self, element, breakelements = None): #this does the same as "CTRL + SHIFT + K"
|
|
if breakelements == None:
|
|
breakelements = []
|
|
if element.tag == inkex.addNS('path','svg'):
|
|
parent = element.getparent()
|
|
idx = parent.index(element)
|
|
idSuffix = 0
|
|
raw = element.path.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:
|
|
replacedelement = copy.copy(element)
|
|
oldId = replacedelement.get('id')
|
|
csp = CubicSuperPath(subpath)
|
|
if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
|
|
replacedelement.set('d', csp)
|
|
if len(subPaths) == 1:
|
|
replacedelement.set('id', oldId)
|
|
else:
|
|
replacedelement.set('id', oldId + str(idSuffix))
|
|
idSuffix += 1
|
|
parent.insert(idx, replacedelement)
|
|
breakelements.append(replacedelement)
|
|
parent.remove(element)
|
|
for child in element.getchildren():
|
|
self.breakContours(child, breakelements)
|
|
return breakelements
|
|
|
|
def effect(self):
|
|
def createLinks(element):
|
|
elementParent = element.getparent()
|
|
path = element.path.to_arrays() #to_arrays() is deprecated. How to make more modern?
|
|
pathIsClosed = False
|
|
if path[-1][0] == 'Z' or \
|
|
(path[-1][0] == 'L' and path[0][1] == path[-1][1]) or \
|
|
(path[-1][0] == 'C' and path[0][1] == [path[-1][1][-2], path[-1][1][-1]]) \
|
|
: #if first is last point the path is also closed. The "Z" command is not required
|
|
pathIsClosed = True
|
|
|
|
if self.options.path_types == 'open_paths' and pathIsClosed is True:
|
|
return #skip this loop iteration
|
|
elif self.options.path_types == 'closed_paths' and pathIsClosed is False:
|
|
return #skip this loop iteration
|
|
elif self.options.path_types == 'both':
|
|
pass
|
|
|
|
# if keeping is enabled we make of copy of the current element and insert it while modifying the original ones. We could also delete the original and modify a copy...
|
|
if self.options.keep_selected is True and self.options.weakening_mode is False:
|
|
parent = element.getparent()
|
|
idx = parent.index(element)
|
|
copyelement = copy.copy(element)
|
|
parent.insert(idx, copyelement)
|
|
|
|
# we measure the length of the path to calculate the required dash configuration
|
|
csp = element.path.transform(element.composed_transform()).to_superpath()
|
|
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
|
|
|
|
if self.options.length_filter is True:
|
|
if stotal < self.svg.unittouu(str(self.options.length_filter_value) + self.options.length_filter_unit):
|
|
if self.options.show_info is True: self.msg("element " + element.get('id') + " is shorter than minimum allowed length of {:1.3f} {}. Path length is {:1.3f} {}".format(self.options.length_filter_value, self.options.length_filter_unit, stotal, self.options.creationunit))
|
|
return #skip this loop iteration
|
|
|
|
if self.options.creationunit == "percent":
|
|
length_link = (self.options.length_link / 100.0) * stotal
|
|
else:
|
|
length_link = self.svg.unittouu(str(self.options.length_link) + self.options.creationunit)
|
|
|
|
dashes = [] #central dashes array
|
|
|
|
if self.options.creationtype == "entered_values":
|
|
dash_length = ((stotal - length_link * self.options.link_count) / self.options.link_count) - 2 * length_link * self.options.link_multiplicator
|
|
dashes.append(dash_length)
|
|
dashes.append(length_link)
|
|
for i in range(0, self.options.link_multiplicator):
|
|
dashes.append(length_link) #stroke (=gap)
|
|
dashes.append(length_link) #gap
|
|
|
|
if self.options.switch_pattern is True:
|
|
dashes = dashes[::-1] #reverse the array
|
|
|
|
#validate dashes. May not be negative (dash or gap cannot be longer than the path itself). Otherwise Inkscape will freeze forever. Reason: rendering issue
|
|
if any(dash <= 0.0 for dash in dashes) == True:
|
|
if self.options.show_info is True: self.msg("element " + element.get('id') + ": Error! Dash array may not contain negative numbers: " + ' '.join(format(dash, "1.3f") for dash in dashes) + ". Path skipped. Maybe it's too short. Adjust your link count, multiplicator and length accordingly, or set to unit '%'")
|
|
return False if self.options.skip_errors is True else exit(1)
|
|
|
|
if self.options.creationunit == "percent":
|
|
stroke_dashoffset = (self.options.link_offset / 100.0 * stotal) - length_link/2
|
|
else:
|
|
stroke_dashoffset = self.svg.unittouu(str(self.options.link_offset) + self.options.creationunit)
|
|
|
|
if self.options.switch_pattern is True:
|
|
stroke_dashoffset = stroke_dashoffset + ((self.options.link_multiplicator * 2) + 1) * length_link
|
|
|
|
if self.options.creationtype == "use_existing":
|
|
if self.options.no_convert is True:
|
|
if self.options.show_info is True: self.msg("element " + element.get('id') + ": Nothing to do. Please select another creation method or disable cosmetic style output paths.")
|
|
return False if self.options.skip_errors is True else exit(1)
|
|
stroke_dashoffset = 0
|
|
style = element.style
|
|
if 'stroke-dashoffset' in style:
|
|
stroke_dashoffset = style['stroke-dashoffset']
|
|
try:
|
|
floats = [float(dash) for dash in re.findall(r"[+]?\d*\.\d+|\d+", style['stroke-dasharray'])] #allow only positive values
|
|
if len(floats) > 0:
|
|
dashes = floats #overwrite previously calculated values with custom input
|
|
else:
|
|
raise ValueError
|
|
except:
|
|
if self.options.show_info is True: self.msg("element " + element.get('id') + ": No dash style to continue with.")
|
|
return False if self.options.skip_errors is True else exit(1)
|
|
|
|
if self.options.creationtype == "custom_dashpattern":
|
|
stroke_dashoffset = self.options.custom_dashoffset_value
|
|
try:
|
|
floats = [float(dash) for dash in re.findall(r"[+]?\d*\.\d+|\d+", self.options.custom_dasharray_value)] #allow only positive values
|
|
if len(floats) > 0:
|
|
dashes = floats #overwrite previously calculated values with custom input
|
|
else:
|
|
raise ValueError
|
|
except:
|
|
if self.options.show_info is True: self.msg("element " + element.get('id') + ": Error in custom dasharray string (might be empty or does not contain any numbers).")
|
|
return False if self.options.skip_errors is True else exit(1)
|
|
|
|
#assign stroke dasharray from entered values, existing style or custom dashpattern
|
|
stroke_dasharray = ' '.join(format(dash, "1.3f") for dash in dashes)
|
|
|
|
# check if the element has a style attribute. If not we create a blank one with a black stroke and without fill
|
|
style = None
|
|
default_fill = 'none'
|
|
default_stroke = '#000000'
|
|
default_stroke_width = str(self.svg.unittouu('1px'))
|
|
if element.attrib.has_key('style'):
|
|
element.style['stroke-dasharray'] = stroke_dasharray
|
|
element.style['stroke-dashoffset'] = stroke_dashoffset
|
|
#if has style attribute but the style attribute does not contain fill, stroke, stroke-width, stroke-dasharray or stroke-dashoffset yet
|
|
if element.style.get('fill') is None: element.style['fill'] = default_fill
|
|
if element.style.get('stroke') is None: element.style['stroke'] = default_stroke
|
|
if element.style.get('stroke-width') is None: element.style['stroke-width'] = default_stroke_width
|
|
else:
|
|
element.style = 'fill:{};stroke:{};stroke-width:{};stroke-dasharray:{};stroke-dashoffset:{};'.format(
|
|
default_fill, default_stroke, default_stroke_width, stroke_dasharray, stroke_dashoffset)
|
|
|
|
#if enabled, we override stroke color with blue (now, as the element definitely has a style)
|
|
if self.options.weakening_mode is True and self.options.switch_pattern is True:
|
|
element.style['stroke'] = "#0000ff"
|
|
|
|
# Print some info about values
|
|
if self.options.show_info is True:
|
|
self.msg("element " + element.get('id') + ":")
|
|
if self.options.creationunit == "percent":
|
|
self.msg(" * total path length = {:1.3f} {}".format(stotal, self.svg.unit)) #show length, converted in selected unit
|
|
self.msg(" * (calculated) offset: {:1.3f} %".format(stroke_dashoffset))
|
|
if self.options.creationtype == "entered_values":
|
|
self.msg(" * (calculated) gap length: {:1.3f} %".format(length_link))
|
|
else:
|
|
self.msg(" * total path length = {:1.3f} {} ({:1.3f} {})".format(self.svg.uutounit(stotal, self.options.creationunit), self.options.creationunit, stotal, self.svg.unit)) #show length, converted in selected unit
|
|
self.msg(" * (calculated) offset: {:1.3f} {}".format(self.svg.uutounit(stroke_dashoffset, self.options.creationunit), self.options.creationunit))
|
|
if self.options.creationtype == "entered_values":
|
|
self.msg(" * (calculated) gap length: {:1.3f} {}".format(length_link, self.options.creationunit))
|
|
if self.options.creationtype == "entered_values":
|
|
self.msg(" * total gaps = {}".format(self.options.link_count))
|
|
self.msg(" * (calculated) dash/gap pattern: {} ({})".format(stroke_dasharray, self.svg.unit))
|
|
|
|
# Conversion step (split cosmetic path into real segments)
|
|
if self.options.no_convert is False:
|
|
style = element.style #get the style again, but this time as style class
|
|
|
|
gaps = []
|
|
new = []
|
|
for sub in element.path.to_superpath():
|
|
idash = 0
|
|
dash = dashes[0]
|
|
length = abs(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:
|
|
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
|
|
idash = (idash + 1) % len(dashes)
|
|
dash = dashes[idash]
|
|
if idash % 2:
|
|
new.append([sub[i]])
|
|
else:
|
|
new[-1].append(sub[i])
|
|
i += 1
|
|
|
|
#filter pointy subpaths
|
|
final_new = []
|
|
for sub in new:
|
|
if len(sub) > 1:
|
|
final_new.append(sub)
|
|
|
|
style.pop('stroke-dasharray')
|
|
element.pop('sodipodi:type')
|
|
element.path = CubicSuperPath(final_new)
|
|
element.style = style
|
|
|
|
# break apart the combined path to have multiple elements
|
|
if self.options.breakapart is True:
|
|
breakOutputelements = None
|
|
breakOutputelements = self.breakContours(element, breakOutputelements)
|
|
breakApartGroup = elementParent.add(inkex.Group())
|
|
for breakOutputelement in breakOutputelements:
|
|
breakApartGroup.append(breakOutputelement)
|
|
#self.msg(replacedelement.get('id'))
|
|
#self.svg.selection.set(replacedelement.get('id')) #update selection to split paths segments (does not work, so commented out)
|
|
|
|
if len(self.svg.selected) > 0:
|
|
for element in self.svg.selection.values():
|
|
#at first we need to break down combined elements to single path, otherwise dasharray cannot properly be applied
|
|
breakInputelements = None
|
|
breakInputelements = self.breakContours(element, breakInputelements)
|
|
for breakInputelement in breakInputelements:
|
|
createLinks(breakInputelement)
|
|
else:
|
|
self.msg('Please select some paths first.')
|
|
return
|
|
|
|
if __name__ == '__main__':
|
|
LinksCreator().run() |