2021-04-21 15:09:36 +02:00
#!/usr/bin/env python3
2021-04-11 12:13:40 +02:00
# coding=utf-8
#
# 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 '
2021-05-05 00:19:27 +02:00
It is a modification of the file addelements . py
2021-04-11 12:13:40 +02:00
It is a modification of the file convert2dash . py
Extension to convert paths into dash - array line
2021-04-14 21:57:54 +02:00
Extension for InkScape 1. X
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 09.04 .2021
Last patch : 14.04 .2021
License : GNU GPL v3
2021-04-11 12:13:40 +02:00
"""
2021-04-12 23:28:02 +02:00
import copy
import re
2021-04-11 12:13:40 +02:00
import inkex
2021-04-13 15:11:57 +02:00
from inkex import bezier , CubicSuperPath , Group , PathElement
2021-04-11 12:13:40 +02:00
from inkex . bezier import csplength
2021-04-12 23:28:02 +02:00
2021-04-11 12:13:40 +02:00
class LinksCreator ( inkex . EffectExtension ) :
2021-04-15 17:03:47 +02:00
def add_arguments ( self , pars ) :
2021-04-19 20:54:38 +02:00
pars . add_argument ( " --tab " )
2021-04-15 17:03:47 +02:00
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 " )
2021-04-17 21:39:48 +02:00
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 " )
2021-04-15 17:03:47 +02:00
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 = False , help = " Performs CTRL + SHIFT + K to break the new output path into it ' s parts " )
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 " )
2021-04-14 23:52:32 +02:00
2021-05-05 00:19:27 +02:00
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 )
2021-04-14 23:52:32 +02:00
idSuffix = 0
2021-05-05 00:19:27 +02:00
raw = element . path . to_arrays ( )
2021-04-14 23:52:32 +02:00
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
2021-04-21 15:09:36 +02:00
subPaths . append ( raw [ prev : ] )
2021-04-14 23:52:32 +02:00
for subpath in subPaths :
2021-05-05 00:19:27 +02:00
replacedelement = copy . copy ( element )
oldId = replacedelement . get ( ' id ' )
replacedelement . set ( ' d ' , CubicSuperPath ( subpath ) )
replacedelement . set ( ' id ' , oldId + str ( idSuffix ) . zfill ( 5 ) )
parent . insert ( idx , replacedelement )
2021-04-14 23:52:32 +02:00
idSuffix + = 1
2021-05-05 00:19:27 +02:00
breakelements . append ( replacedelement )
parent . remove ( element )
for child in element . getchildren ( ) :
self . breakContours ( child , breakelements )
return breakelements
2021-04-11 12:13:40 +02:00
2021-04-12 23:28:02 +02:00
def effect ( self ) :
2021-05-05 00:19:27 +02:00
def createLinks ( element ) :
elementParent = element . getparent ( )
path = element . path . to_arrays ( ) #to_arrays() is deprecated. How to make more modern?
2021-05-19 14:24:08 +02:00
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
2021-04-13 14:44:28 +02:00
pathIsClosed = True
2021-05-19 14:24:08 +02:00
2021-04-13 14:44:28 +02:00
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
2021-04-14 21:57:54 +02:00
2021-05-05 00:19:27 +02:00
# 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...
2021-04-13 14:44:28 +02:00
if self . options . keep_selected is True :
2021-05-05 00:19:27 +02:00
parent = element . getparent ( )
idx = parent . index ( element )
copyelement = copy . copy ( element )
parent . insert ( idx , copyelement )
2021-04-13 14:44:28 +02:00
# we measure the length of the path to calculate the required dash configuration
2021-05-05 00:19:27 +02:00
csp = element . path . transform ( element . composed_transform ( ) ) . to_superpath ( )
2021-04-13 14:44:28 +02:00
slengths , stotal = csplength ( csp ) #get segment lengths and total length of path in document's internal unit
if self . options . length_filter is True :
2021-04-14 11:43:30 +02:00
if stotal < self . svg . unittouu ( str ( self . options . length_filter_value ) + self . options . length_filter_unit ) :
2021-05-05 00:19:27 +02:00
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 ) )
2021-04-13 14:44:28 +02:00
return #skip this loop iteration
2021-04-14 11:43:30 +02:00
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 )
2021-04-13 14:44:28 +02:00
2021-04-14 11:43:30 +02:00
dashes = [ ] #central dashes array
2021-04-13 14:44:28 +02:00
2021-04-17 21:39:48 +02:00
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 )
2021-04-14 23:52:32 +02:00
for i in range ( 0 , self . options . link_multiplicator ) :
dashes . append ( length_link ) #stroke (=gap)
dashes . append ( length_link ) #gap
2021-04-17 21:39:48 +02:00
if self . options . switch_pattern is True :
dashes = dashes [ : : - 1 ] #reverse the array
2021-04-21 15:09:36 +02:00
#validate dashes. May not be negative (dash or gap cannot be longer than the path itself). Otherwise Inkscape will freeze forever. Reason: rendering issue
2021-04-15 00:48:50 +02:00
if any ( dash < = 0.0 for dash in dashes ) == True :
2021-05-05 00:19:27 +02:00
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 ' % ' " )
2021-04-14 23:52:32 +02:00
return False if self . options . skip_errors is True else exit ( 1 )
2021-04-14 14:26:24 +02:00
if self . options . creationunit == " percent " :
stroke_dashoffset = self . options . link_offset / 100.0
else :
stroke_dashoffset = self . svg . unittouu ( str ( self . options . link_offset ) + self . options . creationunit )
2021-04-17 21:39:48 +02:00
if self . options . switch_pattern is True :
stroke_dashoffset = stroke_dashoffset + ( ( self . options . link_multiplicator * 2 ) + 1 ) * length_link
2021-04-14 14:26:24 +02:00
2021-04-14 11:43:30 +02:00
if self . options . creationtype == " use_existing " :
2021-04-14 23:52:32 +02:00
if self . options . no_convert is True :
2021-05-05 00:19:27 +02:00
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. " )
2021-04-14 23:52:32 +02:00
return False if self . options . skip_errors is True else exit ( 1 )
2021-04-14 11:43:30 +02:00
stroke_dashoffset = 0
2021-05-05 00:19:27 +02:00
style = element . style
2021-04-14 11:43:30 +02:00
if ' stroke-dashoffset ' in style :
stroke_dashoffset = style [ ' stroke-dashoffset ' ]
try :
2021-04-14 23:52:32 +02:00
floats = [ float ( dash ) for dash in re . findall ( r " [+]? \ d* \ . \ d+| \ d+ " , style [ ' stroke-dasharray ' ] ) ] #allow only positive values
2021-04-14 11:43:30 +02:00
if len ( floats ) > 0 :
dashes = floats #overwrite previously calculated values with custom input
else :
raise ValueError
except :
2021-05-05 00:19:27 +02:00
if self . options . show_info is True : self . msg ( " element " + element . get ( ' id ' ) + " : No dash style to continue with. " )
2021-04-14 23:52:32 +02:00
return False if self . options . skip_errors is True else exit ( 1 )
2021-04-13 14:44:28 +02:00
2021-04-14 14:26:24 +02:00
if self . options . creationtype == " custom_dashpattern " :
stroke_dashoffset = self . options . custom_dashoffset_value
2021-04-13 14:44:28 +02:00
try :
2021-04-14 23:52:32 +02:00
floats = [ float ( dash ) for dash in re . findall ( r " [+]? \ d* \ . \ d+| \ d+ " , self . options . custom_dasharray_value ) ] #allow only positive values
2021-04-13 14:44:28 +02:00
if len ( floats ) > 0 :
dashes = floats #overwrite previously calculated values with custom input
else :
raise ValueError
except :
2021-05-05 00:19:27 +02:00
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). " )
2021-04-14 23:52:32 +02:00
return False if self . options . skip_errors is True else exit ( 1 )
#assign stroke dasharray from entered values, existing style or custom dashpattern
2021-04-13 14:44:28 +02:00
stroke_dasharray = ' ' . join ( format ( dash , " 1.3f " ) for dash in dashes )
2021-04-14 23:52:32 +02:00
2021-05-05 00:19:27 +02:00
# check if the element has a style attribute. If not we create a blank one with a black stroke and without fill
2021-04-13 14:44:28 +02:00
style = None
2021-04-15 00:48:50 +02:00
default_fill = ' none '
2021-04-14 23:52:32 +02:00
default_stroke_width = ' 1px '
default_stroke = ' #000000 '
2021-05-05 00:19:27 +02:00
if element . attrib . has_key ( ' style ' ) :
style = element . get ( ' style ' )
2021-04-13 14:44:28 +02:00
if style . endswith ( ' ; ' ) is False :
style + = ' ; '
2021-04-12 23:28:02 +02:00
2021-04-15 00:48:50 +02:00
# if has style attribute and dasharray and/or dashoffset are present we modify it accordingly
2021-04-13 14:44:28 +02:00
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 ( )
2021-04-17 21:39:48 +02:00
#if prop == 'fill':
# declarations[i] = prop + ':{}'.format(default_fill)
#if prop == 'stroke':
# declarations[i] = prop + ':{}'.format(default_stroke)
#if prop == 'stroke-width':
# declarations[i] = prop + ':{}'.format(default_stroke_width)
2021-04-13 14:44:28 +02:00
if prop == ' stroke-dasharray ' : #comma separated list of one or more float values
2021-04-15 00:48:50 +02:00
declarations [ i ] = prop + ' : {} ' . format ( stroke_dasharray )
2021-04-13 14:44:28 +02:00
if prop == ' stroke-dashoffset ' :
2021-04-15 00:48:50 +02:00
declarations [ i ] = prop + ' : {} ' . format ( stroke_dashoffset )
2021-05-05 00:19:27 +02:00
element . set ( ' style ' , ' ; ' . join ( declarations ) ) #apply new style to element
2021-04-12 23:28:02 +02:00
2021-04-15 00:48:50 +02:00
#if has style attribute but the style attribute does not contain fill, stroke, stroke-width, stroke-dasharray or stroke-dashoffset yet
2021-05-05 00:19:27 +02:00
style = element . style
2021-04-14 23:52:32 +02:00
if re . search ( ' fill:(.*?)(;|$) ' , str ( style ) ) is None :
2021-04-15 00:48:50 +02:00
style + = ' fill: {} ; ' . format ( default_fill )
2021-04-14 23:52:32 +02:00
if re . search ( ' (;|^)stroke:(.*?)(;|$) ' , str ( style ) ) is None : #if "stroke" is None, add one. We need to distinguish because there's also attribute "-inkscape-stroke" that's why we check starting with ^ or ;
style + = ' stroke: {} ; ' . format ( default_stroke )
if not ' stroke-width ' in style :
style + = ' stroke-width: {} ; ' . format ( default_stroke_width )
2021-04-13 14:44:28 +02:00
if not ' stroke-dasharray ' in style :
2021-04-14 23:52:32 +02:00
style + = ' stroke-dasharray: {} ; ' . format ( stroke_dasharray )
2021-04-13 14:44:28 +02:00
if not ' stroke-dashoffset ' in style :
2021-04-14 23:52:32 +02:00
style + = ' stroke-dashoffset: {} ; ' . format ( stroke_dashoffset )
2021-05-05 00:19:27 +02:00
element . set ( ' style ' , style )
2021-04-13 14:44:28 +02:00
else :
2021-04-15 00:48:50 +02:00
style = ' fill: {} ;stroke: {} ;stroke-width: {} ;stroke-dasharray: {} ;stroke-dashoffset: {} ; ' . format ( default_fill , default_stroke , default_stroke_width , stroke_dasharray , stroke_dashoffset )
2021-05-05 00:19:27 +02:00
element . set ( ' style ' , style )
2021-04-14 23:52:32 +02:00
2021-04-14 11:43:30 +02:00
# Print some info about values
if self . options . show_info is True :
2021-05-05 00:19:27 +02:00
self . msg ( " element " + element . get ( ' id ' ) + " : " )
2021-04-14 11:43:30 +02:00
if self . options . creationunit == " percent " :
2021-04-19 20:54:38 +02:00
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 ) )
2021-04-14 11:43:30 +02:00
if self . options . creationtype == " entered_values " :
2021-04-19 20:54:38 +02:00
self . msg ( " * (calculated) gap length: {:1.3f} % " . format ( length_link ) )
2021-04-14 11:43:30 +02:00
else :
2021-04-19 20:54:38 +02:00
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 ) )
2021-04-14 11:43:30 +02:00
if self . options . creationtype == " entered_values " :
2021-04-19 20:54:38 +02:00
self . msg ( " * (calculated) gap length: {:1.3f} {} " . format ( length_link , self . options . creationunit ) )
2021-04-14 11:43:30 +02:00
if self . options . creationtype == " entered_values " :
2021-04-19 20:54:38 +02:00
self . msg ( " * total gaps = {} " . format ( self . options . link_count ) )
self . msg ( " * (calculated) dash/gap pattern: {} ( {} ) " . format ( stroke_dasharray , self . svg . unit ) )
2021-04-14 11:43:30 +02:00
# Conversion step (split cosmetic path into real segments)
if self . options . no_convert is False :
2021-05-05 00:19:27 +02:00
style = element . style #get the style again, but this time as style class
2021-04-14 11:43:30 +02:00
new = [ ]
2021-05-05 00:19:27 +02:00
for sub in element . path . to_superpath ( ) :
2021-04-14 11:43:30 +02:00
idash = 0
dash = dashes [ 0 ]
length = float ( stroke_dashoffset )
2021-04-11 12:13:40 +02:00
while dash < length :
length = length - dash
idash = ( idash + 1 ) % len ( dashes )
dash = dashes [ idash ]
2021-04-14 11:43:30 +02:00
new . append ( [ sub [ 0 ] [ : ] ] )
i = 1
while i < len ( sub ) :
dash = dash - length
length = bezier . cspseglength ( new [ - 1 ] [ - 1 ] , sub [ i ] )
while dash < length :
2021-04-21 15:09:36 +02:00
new [ - 1 ] [ - 1 ] , nxt , sub [ i ] = bezier . cspbezsplitatlength ( new [ - 1 ] [ - 1 ] , sub [ i ] , dash / length )
if idash % 2 : # create a gap
2021-04-14 11:43:30 +02:00
new . append ( [ nxt [ : ] ] )
2021-04-21 15:09:36 +02:00
else : # splice the curve
2021-04-14 11:43:30 +02:00
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 ' )
2021-05-05 00:19:27 +02:00
element . pop ( ' sodipodi:type ' )
2021-04-21 15:09:36 +02:00
csp = CubicSuperPath ( new )
2021-05-05 00:19:27 +02:00
element . path = CubicSuperPath ( new )
element . style = style
2021-04-14 11:43:30 +02:00
# break apart the combined path to have multiple elements
if self . options . breakapart is True :
2021-05-05 00:19:27 +02:00
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)
2021-04-21 15:09:36 +02:00
#cleanup useless points
2021-05-05 00:19:27 +02:00
p = breakOutputelement . path
2021-04-21 15:09:36 +02:00
commandsCoords = p . to_arrays ( )
# "m 45.250809,91.692739" - this path contains onyl one command - a single point
if len ( commandsCoords ) == 1 :
2021-05-05 00:19:27 +02:00
breakOutputelement . delete ( )
2021-04-21 15:09:36 +02:00
# "m 45.250809,91.692739 z" - this path contains two commands, but only one coordinate.
# It's a single point, the path is closed by a Z command
elif len ( commandsCoords ) == 2 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] :
2021-05-05 00:19:27 +02:00
breakOutputelement . delete ( )
2021-04-21 15:09:36 +02:00
# "m 45.250809,91.692739 l 45.250809,91.692739" - this path contains two commands,
# but the first and second coordinate are the same. It will render als point
elif len ( commandsCoords ) == 2 and commandsCoords [ - 1 ] [ 0 ] == ' Z ' :
2021-05-05 00:19:27 +02:00
breakOutputelement . delete ( )
2021-04-21 15:09:36 +02:00
# "m 45.250809,91.692739 l 45.250809,91.692739 z" - this path contains three commands,
# but the first and second coordinate are the same. It will render als point, the path is closed by a Z command
elif len ( commandsCoords ) == 3 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] and commandsCoords [ 2 ] [ 1 ] == ' Z ' :
2021-05-05 00:19:27 +02:00
breakOutputelement . delete ( )
2021-04-21 15:09:36 +02:00
2021-04-13 14:44:28 +02:00
if len ( self . svg . selected ) > 0 :
2021-05-05 00:19:27 +02:00
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 )
2021-04-12 23:28:02 +02:00
else :
2021-04-19 20:54:38 +02:00
self . msg ( ' Please select some paths first. ' )
2021-04-12 23:28:02 +02:00
return
2021-04-11 12:13:40 +02:00
if __name__ == ' __main__ ' :
LinksCreator ( ) . run ( )