2022-11-05 12:30:28 +01:00
#!/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 ) :
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
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. " )
2023-08-19 17:40:00 +02:00
pars . add_argument ( " --custom_dashoffset_value " , type = float , default = 0.000 , help = " Link offset (+/-) " )
2022-11-05 12:30:28 +01:00
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 )
2023-08-19 17:40:00 +02:00
idSuffix = 0
2022-11-05 12:30:28 +01:00
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 ) :
2023-08-19 17:40:00 +02:00
def createLinks ( element ) :
2022-11-05 12:30:28 +01:00
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
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01: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
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01: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...
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
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
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
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01: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 )
2023-08-19 17:40:00 +02:00
dashes . append ( length_link )
2022-11-05 12:30:28 +01:00
for i in range ( 0 , self . options . link_multiplicator ) :
dashes . append ( length_link ) #stroke (=gap)
dashes . append ( length_link ) #gap
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
if self . options . switch_pattern is True :
dashes = dashes [ : : - 1 ] #reverse the array
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01: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
2023-08-19 17:40:00 +02:00
if any ( dash < = 0.0 for dash in dashes ) == True :
2022-11-05 12:30:28 +01: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 ' % ' " )
return False if self . options . skip_errors is True else exit ( 1 )
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
if self . options . creationunit == " percent " :
stroke_dashoffset = ( self . options . link_offset / 100.0 * stotal ) - length_link / 2
2023-08-19 17:40:00 +02:00
else :
2022-11-05 12:30:28 +01:00
stroke_dashoffset = self . svg . unittouu ( str ( self . options . link_offset ) + self . options . creationunit )
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
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 )
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
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 ) )
2023-08-19 17:40:00 +02:00
if self . options . creationtype == " entered_values " :
2022-11-05 12:30:28 +01:00
self . msg ( " * total gaps = {} " . format ( self . options . link_count ) )
self . msg ( " * (calculated) dash/gap pattern: {} ( {} ) " . format ( stroke_dasharray , self . svg . unit ) )
2023-08-19 17:40:00 +02:00
# Conversion step (split cosmetic path into real segments)
2022-11-05 12:30:28 +01:00
if self . options . no_convert is False :
2023-08-19 17:40:00 +02:00
style = element . style #get the style again, but this time as style class
gaps = [ ]
2022-11-05 12:30:28 +01:00
new = [ ]
for sub in element . path . to_superpath ( ) :
idash = 0
dash = dashes [ 0 ]
2023-08-19 17:40:00 +02:00
length = abs ( float ( stroke_dashoffset ) )
2022-11-05 12:30:28 +01:00
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 ] )
2023-08-19 17:40:00 +02:00
i + = 1
2022-11-05 12:30:28 +01:00
#filter pointy subpaths
final_new = [ ]
for sub in new :
if len ( sub ) > 1 :
final_new . append ( sub )
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
style . pop ( ' stroke-dasharray ' )
element . pop ( ' sodipodi:type ' )
element . path = CubicSuperPath ( final_new )
element . style = style
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
# 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)
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
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
2023-08-19 17:40:00 +02:00
2022-11-05 12:30:28 +01:00
if __name__ == ' __main__ ' :
LinksCreator ( ) . run ( )