2021-04-02 10:20:08 +02:00
#!/usr/bin/env python3
2021-04-02 23:07:28 +02:00
# suppress some nasty warnings we don't want. Note that this is really generic. For developing purposes re-enable this to see errors/deprecations
2021-04-02 10:20:08 +02:00
import logging
for key in logging . Logger . manager . loggerDict :
print ( key )
logging . getLogger ( ) . setLevel ( logging . CRITICAL )
2021-04-02 23:07:28 +02:00
#for name, logger in logging.root.manager.loggerDict.items():
# logger.disabled=True
#import warnings
#warnings.filterwarnings("ignore", category=DeprecationWarning)
#warnings.filterwarnings('always', category=DeprecationWarning)
#with warnings.catch_warnings():
# warnings.simplefilter("ignore", category=DeprecationWarning)
#def noop(*args, **kargs): pass
#warnings.warn = noop
#logging.captureWarnings(True)
2021-04-02 10:20:08 +02:00
import sys
import os
from lxml import etree
import inkex
2021-04-02 23:07:28 +02:00
from inkex import transforms , bezier
2021-04-02 10:20:08 +02:00
from inkex . paths import CubicSuperPath
from inkex . command import inkscape
import vpype
import vpype_viewer
from vpype_viewer import ViewMode
from vpype_cli import execute
from shapely . geometry import LineString , Point
"""
Extension for InkScape 1. X
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
2021-04-02 23:07:28 +02:00
Date : 02.04 .2021
Last patch : 02.04 .2021
2021-04-02 10:20:08 +02:00
License : GNU GPL v3
Used version of vpype : commit id https : / / github . com / abey79 / vpype / commit / 0b0 dc8dd7e32998dbef639f9db578c3bff02690b
CLI / API docs :
- https : / / vpype . readthedocs . io / en / stable / api / vpype_cli . html #module-vpype_cli
- https : / / vpype . readthedocs . io / en / stable / api / vpype . html #module-vpype
vpype commands could be performed differently :
- 1. Work with current selection ( line - wise ) : we could get the selected nodes / groups and check if those nodes are paths . If yes we could convert them to polylines and put it into vpype using doc . add ( LineCollection , Layer )
- 2. We could execute vpype on the complete document only ( svg file handling , possible as one layer or multiple layers )
working line of code ( example : ) doc = vpype . read_multilayer_svg ( self . options . input_file , quantization = 0.1 , crop = False , simplify = False , parallel = False )
Todo ' s
- https : / / github . com / abey79 / vpype / issues / 243
2021-04-02 23:07:28 +02:00
- command chain is slow on Windows
- add some debugging options to remove deprecation warnings
2021-04-02 10:20:08 +02:00
"""
class vpypetools ( inkex . EffectExtension ) :
def __init__ ( self ) :
inkex . Effect . __init__ ( self )
2021-04-02 23:07:28 +02:00
# Line Sorting
self . arg_parser . add_argument ( " --linesort " , default = False , type = inkex . Boolean )
2021-04-02 10:20:08 +02:00
self . arg_parser . add_argument ( " --linesort_no_flip " , type = inkex . Boolean , default = False , help = " Disable reversing stroke direction for optimization " )
2021-04-02 23:07:28 +02:00
# Line Merging
self . arg_parser . add_argument ( " --linemerge " , default = False , type = inkex . Boolean )
self . arg_parser . add_argument ( " --linemerge_tolerance " , type = float , default = False , help = " Maximum distance between two line endings that should be merged (default 0.5 mm) " )
self . arg_parser . add_argument ( " --linemerge_no_flip " , type = inkex . Boolean , default = False , help = " Disable reversing stroke direction for merging " )
# General Settings
self . arg_parser . add_argument ( " --flattenbezier " , type = inkex . Boolean , default = False , help = " Flatten bezier curves to polylines " )
self . arg_parser . add_argument ( " --flatness " , type = float , default = 0.1 , help = " Minimum flatness = 0.1. The smaller the value the more fine segments you will get. " )
2021-04-02 10:20:08 +02:00
self . arg_parser . add_argument ( " --apply_transformations " , type = inkex . Boolean , default = False , help = " Run ' Apply Transformations ' extension before running vpype. Helps avoiding geometry shifting " )
self . arg_parser . add_argument ( " --output_show " , type = inkex . Boolean , default = False , help = " This will open a new matplotlib window showing modified SVG data " )
self . arg_parser . add_argument ( " --output_stats " , type = inkex . Boolean , default = False , help = " Show output statistics before/after conversion " )
self . arg_parser . add_argument ( " --output_trajectories " , type = inkex . Boolean , default = False , help = " Add paths for the travel trajectories " )
2021-04-02 23:07:28 +02:00
self . arg_parser . add_argument ( " --keep_selection " , type = inkex . Boolean , default = False , help = " If false, selected paths will be removed " )
self . arg_parser . add_argument ( " --strokes_to_paths " , type = inkex . Boolean , default = True , help = " Recommended option. Performs ' Path ' > ' Stroke to Path ' (CTRL + ALT + C) to convert vpype converted lines back to regular path objects " )
2021-04-02 10:20:08 +02:00
def effect ( self ) :
lc = vpype . LineCollection ( ) # create a new array of LineStrings consisting of Points. We convert selected paths to polylines and grab their points
nodesToConvert = [ ] # we make an array of all collected nodes to get the boundingbox of that array. We need it to place the vpype converted stuff to the correct XY coordinates
applyTransformAvailable = False
# at first we apply external extension
try :
sys . path . append ( " .. " ) # add parent directory to path to allow importing applytransform (vpype extension is encapsulated in sub directory)
import applytransform
applyTransformAvailable = True
except Exception as e :
# inkex.utils.debug(e)
inkex . utils . debug ( " Calling ' Apply Transformations ' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping this step " )
2021-04-02 23:07:28 +02:00
def flatten ( node ) :
path = node . path . to_superpath ( )
bezier . cspsubdiv ( path , self . options . flatness )
newpath = [ ]
for subpath in path :
first = True
for csp in subpath :
cmd = ' L '
if first :
cmd = ' M '
first = False
newpath . append ( [ cmd , [ csp [ 1 ] [ 0 ] , csp [ 1 ] [ 1 ] ] ] )
node . path = newpath
2021-04-02 10:20:08 +02:00
def convertPath ( node ) :
if node . tag == inkex . addNS ( ' path ' , ' svg ' ) :
nodesToConvert . append ( node )
2021-04-02 23:07:28 +02:00
if self . options . flattenbezier is True :
flatten ( node )
2021-04-02 10:20:08 +02:00
d = node . get ( ' d ' )
p = CubicSuperPath ( d )
points = [ ]
for subpath in p :
for csp in subpath :
points . append ( Point ( csp [ 1 ] [ 0 ] , csp [ 1 ] [ 1 ] ) )
lc . append ( LineString ( points ) )
children = node . getchildren ( )
if children is not None :
for child in children :
convertPath ( child )
# inkex.utils.debug(str(applyTransformAvailable)) #check if ApplyTransform Extension is available. If yes we use it
if self . options . apply_transformations is True and applyTransformAvailable is True :
applytransform . ApplyTransform ( ) . recursiveFuseTransform ( self . document . getroot ( ) )
# getting the bounding box of the current selection. We use to calculate the offset XY from top-left corner of the canvas. This helps us placing back the elements
bbox = None
if len ( self . svg . selected ) == 0 :
convertPath ( self . document . getroot ( ) )
for element in nodesToConvert :
bbox + = element . bounding_box ( )
else :
for id , item in self . svg . selected . items ( ) :
convertPath ( item )
bbox = inkex . elements . _selected . ElementList . bounding_box ( self . svg . selected ) # get BoundingBox for selection
# inkex.utils.debug(bbox)
#l c.as_mls() #cast LineString array to MultiLineString
if len ( lc ) == 0 :
2021-04-02 23:07:28 +02:00
inkex . errormsg ( ' Selection appears to be empty or does not contain any valid svg:path nodes. Try to cast your objects to paths using CTRL + SHIFT + C or strokes to paths using CTRL + ALT+ C ' )
2021-04-02 10:20:08 +02:00
return
doc = vpype . Document ( ) #create new vpype document
# we add the lineCollection (converted selection) to the vpype document
doc . add ( lc , layer_id = None )
if self . options . output_stats is True :
tooling_length_before = doc . length ( )
traveling_length_before = doc . pen_up_length ( )
# build and execute the conversion command
2021-04-02 23:07:28 +02:00
##########################################
# Line Sort
if self . options . linesort is True :
command = " linesort "
if self . options . linesort_no_flip is True :
command + = " --no-flip "
# Line Merging
if self . options . linemerge is True :
command = " linemerge --tolerance " + str ( self . options . linemerge_tolerance )
if self . options . linemerge_no_flip is True :
command + = " --no-flip "
#inkex.utils.debug(command)
2021-04-02 10:20:08 +02:00
doc = execute ( command , doc )
2021-04-02 23:07:28 +02:00
##########################################
2021-04-02 10:20:08 +02:00
# show the vpype document visually
# there are missing options to set pen_width and pen_opacity. This is anchored in "Engine" class
if self . options . output_show :
vpype_viewer . show ( doc , view_mode = ViewMode . PREVIEW , show_pen_up = self . options . output_trajectories , show_points = False , argv = None ) # https://vpype.readthedocs.io/en/stable/api/vpype_viewer.ViewMode.html
if self . options . output_stats is True :
tooling_length_after = doc . length ( )
traveling_length_after = doc . pen_up_length ( )
if tooling_length_before > 0 :
tooling_length_saving = ( 1.0 - tooling_length_after / tooling_length_before ) * 100.0
else :
tooling_length_saving = 0.0
if traveling_length_before > 0 :
traveling_length_saving = ( 1.0 - traveling_length_after / traveling_length_before ) * 100.0
else :
traveling_length_saving = 0.0
inkex . utils . debug ( ' Total tooling length before vpype conversion: ' + str ( ' {:0.2f} ' . format ( tooling_length_before ) ) + ' mm ' )
inkex . utils . debug ( ' Total traveling length before vpype conversion: ' + str ( ' {:0.2f} ' . format ( traveling_length_before ) ) + ' mm ' )
inkex . utils . debug ( ' Total tooling length after vpype conversion: ' + str ( ' {:0.2f} ' . format ( tooling_length_after ) ) + ' mm ' )
inkex . utils . debug ( ' Total traveling length after vpype conversion: ' + str ( ' {:0.2f} ' . format ( traveling_length_after ) ) + ' mm ' )
inkex . utils . debug ( ' Total tooling length optimized: ' + str ( ' {:0.2f} ' . format ( tooling_length_saving ) ) + ' % ' )
inkex . utils . debug ( ' Total traveling length optimized: ' + str ( ' {:0.2f} ' . format ( traveling_length_saving ) ) + ' % ' )
# save the vpype document to new svg file and close it afterwards
output_file = self . options . input_file + " .vpype.svg "
output_fileIO = open ( output_file , " w " , encoding = " utf-8 " )
vpype . write_svg ( output_fileIO , doc , page_size = None , center = False , source_string = ' ' , layer_label_format = ' %d ' , show_pen_up = self . options . output_trajectories , color_mode = ' layer ' )
output_fileIO . close ( )
# convert vpype polylines/lines/polygons to regular paths again. We need to use "--with-gui" to respond to "WARNING: ignoring verb FileSave - GUI required for this verb."
2021-04-02 23:07:28 +02:00
if self . options . strokes_to_paths is True :
cli_output = inkscape ( output_file , " --with-gui " , actions = " EditSelectAllInAllLayers;EditUnlinkClone;ObjectToPath;FileSave;FileQuit " )
if len ( cli_output ) > 0 :
self . debug ( _ ( " Inkscape returned the following output when trying to run the vpype object to path back-conversion: " ) )
self . debug ( cli_output )
2021-04-02 10:20:08 +02:00
# new parse the SVG file and insert it as new group into the current document tree
2021-04-02 23:07:28 +02:00
#vpype_svg = etree.parse(output_file).getroot().xpath("//svg:g", namespaces=inkex.NSS)
# the label id is the number of layer_id=None (will start with 1)
lines = etree . parse ( output_file ) . getroot ( ) . xpath ( " //svg:g[@inkscape:label= ' 1 ' ] " , namespaces = inkex . NSS )
vpypeLinesGroup = self . document . getroot ( ) . add ( inkex . Group ( ) )
vpypeLinesGroup . set ( ' style ' , ' stroke:#000000;stroke-width:1px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none ' )
for item in lines :
for child in item . getchildren ( ) :
vpypeLinesGroup . append ( child )
vpypeLinesGroup . attrib [ ' transform ' ] = ' translate( ' + str ( bbox . left ) + ' , ' + str ( bbox . top ) + ' ) '
vpypeLinesGroupId = self . svg . get_unique_id ( ' vpypetools-lines- ' )
vpypeLinesGroup . set ( ' id ' , vpypeLinesGroupId )
if self . options . output_trajectories is True :
trajectories = etree . parse ( output_file ) . getroot ( ) . xpath ( " //svg:g[@id= ' pen_up_trajectories ' ] " , namespaces = inkex . NSS )
vpypeTrajectoriesGroup = self . document . getroot ( ) . add ( inkex . Group ( ) )
vpypeTrajectoriesGroup . set ( ' style ' , ' stroke:#0000ff;stroke-width:1px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none ' )
for item in trajectories :
for child in item . getchildren ( ) :
vpypeTrajectoriesGroup . append ( child )
vpypeTrajectoriesGroup . attrib [ ' transform ' ] = ' translate( ' + str ( bbox . left ) + ' , ' + str ( bbox . top ) + ' ) '
vpypeTrajectoriesId = self . svg . get_unique_id ( ' vpypetools-trajectories- ' )
vpypeTrajectoriesGroup . set ( ' id ' , vpypeTrajectoriesId )
2021-04-02 10:20:08 +02:00
# inkex.utils.debug(self.svg.selection.first()) # get the first selected element. Chould be None
2021-04-02 23:07:28 +02:00
self . svg . selection . set ( vpypeLinesGroupId )
2021-04-02 10:20:08 +02:00
#inkex.utils.debug(self.svg.selection.first()) # get the first selected element again to check if changing selection has worked
# we apply transformations also for new group to remove the "translate()" again
if self . options . apply_transformations and applyTransformAvailable :
for node in self . svg . selection :
applytransform . ApplyTransform ( ) . recursiveFuseTransform ( node )
# Delete the temporary file again because we do not need it anymore
if os . path . exists ( output_file ) :
os . remove ( output_file )
# Remove selection objects to do a real replace with new objects from vpype document
if self . options . keep_selection is False :
for node in nodesToConvert :
node . getparent ( ) . remove ( node )
if __name__ == ' __main__ ' :
2021-04-02 23:07:28 +02:00
vpypetools ( ) . run ( )