2021-10-23 02:32:35 +02:00
#!/usr/bin/env python3
import inkex
from inkex . bezier import csplength , csparea
from lxml import etree
import re
import math
2021-11-07 05:33:59 +01:00
from math import log
2021-11-03 03:52:37 +01:00
import datetime
2021-11-07 12:55:58 +01:00
from email . policy import default
2021-10-23 02:32:35 +02:00
class LaserCheck ( inkex . EffectExtension ) :
'''
2021-11-02 02:41:57 +01:00
ToDos :
2021-11-07 05:33:59 +01:00
- add some extra seconds for start , stop , removing parts , attaching material , . . .
- maybe remove totalTravelLength and set manually . . .
2021-11-05 01:46:58 +01:00
- Handlungsempfehlungen einbauen
- verweisen auf diverse plugins , die man nutzen kann :
- migrate ungrouper
- pointy paths
- cleaner
- styles to layers
- apply transforms
- epilog bbox adjust
- wege zum Pfade fixen :
- cut slower ( > muss aber auch leistung reduzieren - inb welchem umfang ? )
- sort
- chaining with touching neighbours
- remove path
- remove modes / simplify
- find duplicate lines
- visualize results as a nice SVG rendered check list page with
- red / green / grey icons ( failed , done , skipped ) and calculate some scores
- preview image
- statistics
- export as PDF
- run as script to generate quick results for users
- check for old styles which should be upgraded
- self - intersecting paths
- number of parts ( isles ) to weed in total
- number of parts which are smaller than vector grid
- add some inkex . Desc to all elements which were checked and which have some issue . use special syntax to remove old stuff each time the check is applied again
- this code is horrible ugly stuff
- output time / cost estimations per stroke color
- add check for stroke colors - > make some useful predefinitions like ( for default modes )
- black = general cutting
- blue = cutting inside
- green = cutting outside
- pink = vector engraving
2021-10-23 02:32:35 +02:00
'''
2021-10-23 22:42:22 +02:00
def add_arguments ( self , pars ) :
pars . add_argument ( ' --tab ' )
2021-11-03 03:52:37 +01:00
pars . add_argument ( ' --machine_size ' , default = " 812x508 " )
pars . add_argument ( ' --max_cutting_speed ' , type = float , default = 500 )
pars . add_argument ( ' --max_travel_speed ' , type = float , default = 150 )
pars . add_argument ( ' --cut_travel_factor ' , type = float , default = 0.60 )
pars . add_argument ( ' --price_per_minute_gross ' , type = float , default = 2.0 )
pars . add_argument ( ' --vector_grid_xy ' , type = float , default = 12.0 ) #TODO
2021-11-02 20:34:09 +01:00
pars . add_argument ( ' --show_issues_only ' , type = inkex . Boolean , default = False )
2021-10-23 22:42:22 +02:00
pars . add_argument ( ' --checks ' , default = " check_all " )
pars . add_argument ( ' --bbox ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --bbox_offset ' , type = float , default = 5.000 )
2021-11-02 20:34:09 +01:00
pars . add_argument ( ' --cutting_estimation ' , type = inkex . Boolean , default = False )
2021-11-02 19:28:26 +01:00
pars . add_argument ( ' --elements_outside_canvas ' , type = inkex . Boolean , default = False )
2021-10-23 22:42:22 +02:00
pars . add_argument ( ' --groups_and_layers ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --clones ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --clippaths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --images ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --texts ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --lowlevelstrokes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --stroke_colors ' , type = inkex . Boolean , default = False )
2021-11-02 02:41:57 +01:00
pars . add_argument ( ' --stroke_colors_max ' , type = int , default = 3 )
2021-10-23 22:42:22 +02:00
pars . add_argument ( ' --stroke_widths ' , type = inkex . Boolean , default = False )
2021-11-02 02:41:57 +01:00
pars . add_argument ( ' --stroke_widths_max ' , type = int , default = 1 )
2021-10-23 22:42:22 +02:00
pars . add_argument ( ' --stroke_opacities ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --cosmestic_dashes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --invisible_shapes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --pointy_paths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --transformations ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --short_paths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --short_paths_min ' , type = float , default = 1.000 )
pars . add_argument ( ' --non_path_shapes ' , type = inkex . Boolean , default = False )
2021-11-02 02:41:57 +01:00
pars . add_argument ( ' --nodes_per_path ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --nodes_per_path_max ' , type = int , default = 2 )
pars . add_argument ( ' --nodes_per_path_interval ' , type = float , default = 10.000 )
2021-10-23 22:42:22 +02:00
2021-10-23 02:32:35 +02:00
def effect ( self ) :
2021-10-23 22:42:22 +02:00
so = self . options
2021-11-03 03:52:37 +01:00
machineWidth = self . svg . unittouu ( so . machine_size . split ( ' x ' ) [ 0 ] + " mm " )
machineHeight = self . svg . unittouu ( so . machine_size . split ( ' x ' ) [ 1 ] + " mm " )
2021-11-02 01:54:23 +01:00
selected = [ ] #total list of elements to parse
2021-11-03 03:52:37 +01:00
2021-10-23 22:42:22 +02:00
2021-10-23 02:32:35 +02:00
def parseChildren ( element ) :
if element not in selected :
selected . append ( element )
children = element . getchildren ( )
if children is not None :
for child in children :
if child not in selected :
selected . append ( child )
2021-11-02 01:54:23 +01:00
parseChildren ( child ) #go deeper and deeper
2021-10-23 02:32:35 +02:00
#check if we have selected elements or if we should parse the whole document instead
if len ( self . svg . selected ) == 0 :
for element in self . document . getroot ( ) . iter ( tag = etree . Element ) :
if element != self . document . getroot ( ) :
selected . append ( element )
else :
for element in self . svg . selected . values ( ) :
2021-10-26 10:39:15 +02:00
parseChildren ( element )
2021-11-02 01:54:23 +01:00
2021-10-23 02:32:35 +02:00
namedView = self . document . getroot ( ) . find ( inkex . addNS ( ' namedview ' , ' sodipodi ' ) )
2021-11-01 23:31:07 +01:00
doc_units = namedView . get ( inkex . addNS ( ' document-units ' , ' inkscape ' ) )
user_units = namedView . get ( inkex . addNS ( ' units ' ) )
2021-10-23 22:42:22 +02:00
pagecolor = namedView . get ( ' pagecolor ' )
inkex . utils . debug ( " ---------- Default checks " )
2021-10-23 02:32:35 +02:00
inkex . utils . debug ( " Document units: {} " . format ( doc_units ) )
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " User units: {} " . format ( user_units ) )
2021-10-31 23:39:07 +01:00
2021-10-26 10:39:15 +02:00
'''
The SVG format is highly complex and offers a lot of possibilities . Most things of SVG we do not
need for a laser cutter . Usually we need svg : path and maybe svg : image ; we can drop a lot of stuff
like svg : defs , svg : desc , gradients , etc .
'''
2021-10-23 02:32:35 +02:00
nonShapes = [ ]
shapes = [ ]
for element in selected :
if not isinstance ( element , inkex . ShapeElement ) :
nonShapes . append ( element )
else :
2021-10-26 10:39:15 +02:00
shapes . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} shape elements in total " . format ( len ( shapes ) ) )
inkex . utils . debug ( " {} non-shape elements in total " . format ( len ( nonShapes ) ) )
2021-10-23 02:32:35 +02:00
for nonShape in nonShapes :
inkex . utils . debug ( " non-shape id= {} " . format ( nonShape . get ( ' id ' ) ) )
2021-10-23 22:42:22 +02:00
2021-10-31 23:39:07 +01:00
'''
Nearly each laser job needs a bit of border to place the material inside the laser . Often
we have to fixate on vector grid , pin grid or task plate . Thus we need tapes or pins . So we
leave some borders off the actual part geometries .
'''
if so . checks == " check_all " or so . bbox is True :
inkex . utils . debug ( " \n ---------- Borders around all elements - minimum offset {} mm from each side " . format ( so . bbox_offset ) )
bbox = inkex . BoundingBox ( )
for element in self . document . getroot ( ) . iter ( tag = etree . Element ) :
if element != self . document . getroot ( ) and isinstance ( element , inkex . ShapeElement ) and element . tag != inkex . addNS ( ' use ' , ' svg ' ) and element . get ( ' inkscape:groupmode ' ) != ' layer ' : #bbox fails for svg:use elements and layers
transform = inkex . Transform ( )
parent = element . getparent ( )
if parent is not None and isinstance ( parent , inkex . ShapeElement ) :
transform = parent . composed_transform ( )
try :
if isinstance ( element , inkex . Rectangle ) or \
isinstance ( element , inkex . Circle ) or \
isinstance ( element , inkex . Ellipse ) :
bbox + = element . bounding_box ( ) * scale_factor
elif isinstance ( element , inkex . TextElement ) or \
isinstance ( element , inkex . Tspan ) :
continue
else :
bbox + = element . bounding_box ( transform )
except Exception :
transform = element . composed_transform ( )
x1 , y1 = transform . apply_to_point ( [ 0 , 0 ] )
x2 , y2 = transform . apply_to_point ( [ 1 , 1 ] )
bbox + = inkex . BoundingBox ( ( x1 , x2 ) , ( y1 , y2 ) )
if abs ( bbox . width ) == math . inf or abs ( bbox . height ) == math . inf :
inkex . utils . debug ( " bounding box could not be calculated. SVG seems to be empty. " )
#else:
# inkex.utils.debug("bounding box is {}".format(bbox))
scale_factor = self . svg . unittouu ( " 1px " )
page_width = self . svg . unittouu ( self . document . getroot ( ) . attrib [ ' width ' ] )
width_height = self . svg . unittouu ( self . document . getroot ( ) . attrib [ ' height ' ] )
fmm = self . svg . unittouu ( str ( so . bbox_offset ) + " mm " )
bb_left = round ( bbox . left , 3 )
bb_right = round ( bbox . right , 3 )
bb_top = round ( bbox . top , 3 )
bb_bottom = round ( bbox . bottom , 3 )
bb_width = round ( bbox . width , 3 )
bb_height = round ( bbox . height , 3 )
if bb_left > = fmm :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " left border... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " left border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bb_left , " mm " ) ) )
if bb_top > = fmm :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " top border... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " top border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bb_top , " mm " ) ) )
if bb_right + fmm < = page_width :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " right border... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " right border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bb_right , " mm " ) ) )
if bb_bottom + fmm < = width_height :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " bottom border... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " bottom border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bb_bottom , " mm " ) ) )
if bb_width < = machineWidth :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " page width... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " page width... fail: {:0.3f} mm " . format ( bb_width ) )
if bb_height < = machineHeight :
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " page height... ok " )
2021-10-31 23:39:07 +01:00
else :
inkex . utils . debug ( " page height... fail: {:0.3f} mm " . format ( bb_height ) )
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . groups_and_layers is True :
2021-11-02 01:54:23 +01:00
inkex . utils . debug ( " \n ---------- Groups and layers " )
global md
md = 0
def maxDepth ( element , level ) :
global md
if ( level == md ) :
md + = 1
for child in element :
maxDepth ( child , level + 1 )
maxDepth ( self . document . getroot ( ) , - 1 )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " Maximum group depth= {} " . format ( md - 1 ) )
2021-11-02 01:54:23 +01:00
if md - 1 > 2 :
self . msg ( " Warning: this group depth might cause issues! " )
2021-10-23 22:42:22 +02:00
groups = [ ]
layers = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' g ' , ' svg ' ) :
if element . get ( ' inkscape:groupmode ' ) == ' layer ' :
layers . append ( element )
else :
groups . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} groups in total " . format ( len ( groups ) ) )
inkex . utils . debug ( " {} layers in total " . format ( len ( layers ) ) )
2021-10-23 22:42:22 +02:00
#check for empty groups
for group in groups :
if len ( group ) == 0 :
inkex . utils . debug ( " id= {} is empty group " . format ( group . get ( ' id ' ) ) )
#check for empty layers
for layer in layers :
if len ( layer ) == 0 :
inkex . utils . debug ( " id= {} is empty layer " . format ( layer . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-26 10:39:15 +02:00
'''
Clones should be unlinked because they cause similar issues like transformations
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . clones is True :
inkex . utils . debug ( " \n ---------- Clones (svg:use) - maybe unlink " )
uses = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' use ' , ' svg ' ) :
uses . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} svg:use clones in total " . format ( len ( uses ) ) )
2021-10-23 22:42:22 +02:00
for use in uses :
inkex . utils . debug ( " id= {} " . format ( use . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Clip paths are neat to visualize things , but they do not perform a real path cutting .
Please perform real intersections to have an intact target geometry .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . clippaths is True :
inkex . utils . debug ( " \n ---------- Clippings (svg:clipPath) - please replace with real cut paths " )
clipPaths = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' clipPath ' , ' svg ' ) :
clipPaths . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} svg:clipPath in total " . format ( len ( clipPaths ) ) )
2021-10-23 22:42:22 +02:00
for clipPath in clipPaths :
inkex . utils . debug ( " id= {} " . format ( clipPath . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Sometimes images look like vector but they are ' nt. In case you dont want to perform engraving, either
check to drop or trace to vector paths
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . images is True :
inkex . utils . debug ( " \n ---------- Images (svg:image) - maybe trace to svg " )
images = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' image ' , ' svg ' ) :
images . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} svg:image in total " . format ( len ( images ) ) )
2021-10-23 22:42:22 +02:00
for image in images :
inkex . utils . debug ( " image id= {} " . format ( image . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Low level strokes cannot be properly edited in Inkscape ( no node handles ) . Converting helps
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . lowlevelstrokes is True :
inkex . utils . debug ( " \n ---------- Low level strokes (svg:line/polyline/polygon) - maybe convert to path " )
lowlevels = [ ]
for element in selected :
if element . tag in ( inkex . addNS ( ' line ' , ' svg ' ) , inkex . addNS ( ' polyline ' , ' svg ' ) , inkex . addNS ( ' polygon ' , ' svg ' ) ) :
lowlevels . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} low level strokes in total " . format ( len ( lowlevels ) ) )
2021-10-23 22:42:22 +02:00
for lowlevel in lowlevels :
inkex . utils . debug ( " id= {} " . format ( lowlevel . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Texts cause problems when sharing with other people . You must ensure that everyone has the
font files installed you used . Convert to paths avoids this issue and guarantees same result
everywhere .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . texts is True :
inkex . utils . debug ( " \n ---------- Texts (should be converted to paths) " )
texts = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' text ' , ' svg ' ) :
texts . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} svg:text in total " . format ( len ( texts ) ) )
2021-10-23 22:42:22 +02:00
for text in texts :
inkex . utils . debug ( " id= {} " . format ( text . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
The more stroke colors the more laser job configuration is required . Reduce the SVG file
to a minimum of stroke colors to be quicker
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_colors is True :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " \n ---------- Stroke colors ( {} are allowed) " . format ( so . stroke_colors_max ) )
2021-10-23 22:42:22 +02:00
strokeColors = [ ]
2021-11-07 12:55:58 +01:00
for element in shapes :
strokeColor = element . style . get ( ' stroke ' )
if strokeColor is None or strokeColor == " none " :
strokeColor = " none "
if strokeColor not in strokeColors :
strokeColors . append ( strokeColor )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} different stroke colors in total " . format ( len ( strokeColors ) ) )
2021-11-02 02:41:57 +01:00
if len ( strokeColors ) > so . stroke_colors_max :
for strokeColor in strokeColors :
inkex . utils . debug ( " stroke color {} " . format ( strokeColor ) )
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
Different stroke widths might behave the same like different stroke colors . Reduce to a minimum set .
Ideally all stroke widths are set to 1 pixel .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_widths is True :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " \n ---------- Stroke widths ( {} are allowed) " . format ( so . stroke_widths_max ) )
2021-10-23 22:42:22 +02:00
strokeWidths = [ ]
2021-11-07 12:55:58 +01:00
for element in shapes :
strokeWidth = element . style . get ( ' stroke-width ' )
if strokeWidth is None or strokeWidth == " none " :
strokeWidth = " none "
if strokeWidth not in strokeWidths :
strokeWidths . append ( strokeWidth )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} different stroke widths in total " . format ( len ( strokeWidths ) ) )
2021-11-02 02:41:57 +01:00
if len ( strokeWidths ) > so . stroke_widths_max :
for strokeWidth in strokeWidths :
swConverted = self . svg . uutounit ( float ( self . svg . unittouu ( strokeWidth ) ) ) #possibly w/o units. we unify to some internal float
inkex . utils . debug ( " stroke width {} px ( {} mm) " . format (
round ( self . svg . uutounit ( swConverted , " px " ) , 4 ) ,
round ( self . svg . uutounit ( swConverted , " mm " ) , 4 ) ,
)
2021-11-01 19:28:08 +01:00
)
2021-10-26 10:39:15 +02:00
'''
Cosmetic dashes cause simulation issues and are no real cut paths . It ' s similar to the thing
with clip paths . Please convert lines to real dash segments if you want to laser them .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . cosmestic_dashes is True :
inkex . utils . debug ( " \n ---------- Cosmetic dashes - should be converted to paths " )
strokeDasharrays = [ ]
2021-11-07 12:55:58 +01:00
for element in shapes :
strokeDasharray = element . style . get ( ' stroke-dasharray ' )
if strokeDasharray is None or strokeDasharray == " none " :
strokeDasharray = " none "
if strokeDasharray not in strokeDasharrays :
strokeDasharrays . append ( strokeDasharray )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} different stroke dash arrays in total " . format ( len ( strokeDasharrays ) ) )
2021-10-23 22:42:22 +02:00
for strokeDasharray in strokeDasharrays :
inkex . utils . debug ( " stroke dash array {} " . format ( strokeDasharray ) )
2021-10-23 02:32:35 +02:00
2021-10-26 10:39:15 +02:00
'''
Shapes / paths with the same color like the background , 0 % opacity , etc . lead to strange
laser cutting results , like duplicated edges , enlarged laser times and more . Please double
check for such occurences .
2021-11-07 12:55:58 +01:00
Please transfer styles from layers / groups level to element level ! You can use " Cleanup Styles " extension to do that
2021-10-26 10:39:15 +02:00
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . invisible_shapes is True :
inkex . utils . debug ( " \n ---------- Invisible shapes " )
invisibles = [ ]
for element in shapes :
2021-11-07 12:55:58 +01:00
if element . tag not in ( inkex . addNS ( ' tspan ' , ' svg ' ) ) and element . get ( ' inkscape:groupmode ' ) != ' layer ' and not isinstance ( element , inkex . Group ) :
stroke = element . style . get ( ' stroke ' )
if stroke is None or stroke == " none " :
strokeVis = 0
elif stroke in ( ' #ffffff ' , ' white ' , ' rgb(255,255,255) ' ) :
strokeVis = 0
else :
strokeVis = 1
stroke_width = element . style . get ( ' stroke-width ' )
if stroke_width is None or stroke_width == " none " :
widthVis = 0
elif self . svg . unittouu ( stroke_width ) < 0.005 : #really thin (0,005pc = 0,080px)
widthVis = 0
else :
widthVis = 1
stroke_opacity = element . style . get ( ' stroke-opacity ' )
if stroke_opacity is None or stroke_opacity == " none " :
strokeOpacityVis = 0
elif float ( stroke_opacity ) < 0.05 : #nearly invisible (<5% opacity)
strokeOpacityVis = 0
else :
strokeOpacityVis = 1
if pagecolor == ' #ffffff ' :
invisColors = [ pagecolor , ' white ' , ' rgb(255,255,255) ' ]
else :
invisColors = [ pagecolor ] #we could add some parser to convert pagecolor to rgb/hsl/cmyk
2021-10-23 22:42:22 +02:00
2021-11-07 12:55:58 +01:00
fill = element . style . get ( ' fill ' )
if fill is None or fill == " none " :
fillVis = 0
elif fill in invisColors :
fillVis = 0
else :
fillVis = 1
fill_opacity = element . style . get ( ' fill-opacity ' )
if fill_opacity is None or fill_opacity == " none " : #always is opaque if not set, so set to 1
fillOpacityVis = 1
elif float ( fill_opacity ) < 0.05 : #nearly invisible (<5% opacity)
fillOpacityVis = 0
else :
fillOpacityVis = 1
inkex . utils . debug ( " id= {} , strokeVis= {} , widthVis= {} , strokeOpacityVis= {} , fillVis= {} , fillOpacityVis= {} " . format ( element . get ( ' id ' ) , strokeVis , widthVis , strokeOpacityVis , fillVis , fillOpacityVis ) )
if element . style is not None : #f if the style attribute is not set at all, the element will be visible with default black color fill and w/o stroke
2021-10-23 22:42:22 +02:00
if ( strokeVis == 0 or widthVis == 0 or strokeOpacityVis == 0 ) and ( fillVis == 0 or fillOpacityVis == 0 ) :
if element not in invisibles :
invisibles . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} invisible shapes in total " . format ( len ( invisibles ) ) )
2021-10-23 22:42:22 +02:00
for invisible in invisibles :
inkex . utils . debug ( " id= {} " . format ( invisible . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Additionally , stroke opacities less than 1.0 cause problems in most laser softwares . Please
adjust all strokes to use full opacity .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_opacities is True :
inkex . utils . debug ( " \n ---------- Objects with stroke transparencies < 1.0 - should be set to 1.0 " )
transparencies = [ ]
for element in shapes :
2021-11-07 12:55:58 +01:00
stroke_opacity = element . style . get ( ' stroke-opacity ' )
if stroke_opacity is None or stroke_opacity == " none " :
stroke_opacity = " none "
if stroke_opacity not in transparencies :
transparencies . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} objects with stroke transparencies < 1.0 in total " . format ( len ( transparencies ) ) )
2021-10-23 22:42:22 +02:00
for transparency in transparencies :
inkex . utils . debug ( " id= {} " . format ( transparency . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
We look for paths which are just points . Those are useless in case of lasercutting .
Note : this scan only works for paths , not for subpaths . If so , you need to break apart before
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . pointy_paths is True :
inkex . utils . debug ( " \n ---------- Pointy paths - should be deleted " )
pointyPaths = [ ]
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
p = element . path
commandsCoords = p . to_arrays ( )
if len ( commandsCoords ) == 1 or \
( len ( commandsCoords ) == 2 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] ) or \
( len ( commandsCoords ) == 2 and commandsCoords [ - 1 ] [ 0 ] == ' Z ' ) or \
( len ( commandsCoords ) == 3 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] and commandsCoords [ 2 ] [ 1 ] == ' Z ' ) :
pointyPaths . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} pointy paths in total " . format ( len ( pointyPaths ) ) )
2021-10-23 22:42:22 +02:00
for pointyPath in pointyPaths :
inkex . utils . debug ( " id= {} " . format ( pointyPath . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
Transformations often lead to wrong stroke widths or mis - rendering in end software . The best we
can do with a final SVG is to remove all relative translations , rotations and scalings . We should
apply absolute coordinates only .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . transformations is True :
inkex . utils . debug ( " \n ---------- Transformations - should be applied to absolute " )
transformations = [ ]
for element in shapes :
if element . get ( ' transform ' ) is not None :
transformations . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} transformation in total " . format ( len ( transformations ) ) )
2021-10-23 22:42:22 +02:00
for transformation in transformations :
inkex . utils . debug ( " transformation in id= {} " . format ( transformation . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Really short paths can cause issues with laser cutter mechanics and should be avoided to
have healthier stepper motor belts , etc .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . short_paths is True :
inkex . utils . debug ( " \n ---------- Short paths (< {} mm) " . format ( so . short_paths_min ) )
shortPaths = [ ]
totalLength = 0
totalDropLength = 0
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
slengths , stotal = csplength ( element . path . transform ( element . composed_transform ( ) ) . to_superpath ( ) )
totalLength + = stotal
if stotal < self . svg . unittouu ( str ( so . short_paths_min ) + " mm " ) :
2021-11-01 23:31:07 +01:00
shortPaths . append ( [ element , stotal ] )
2021-10-23 22:42:22 +02:00
totalDropLength + = stotal
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} short paths in total " . format ( len ( shortPaths ) ) )
2021-11-02 02:41:57 +01:00
if totalDropLength > 0 :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {:0.2f} % o f total ( {:0.2f} mm / {:0.2f} mm) " . format ( totalDropLength / totalLength , self . svg . uutounit ( str ( totalDropLength ) , " mm " ) , self . svg . uutounit ( str ( totalLength ) , " mm " ) ) )
2021-10-23 22:42:22 +02:00
for shortPath in shortPaths :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " id= {} , length= {} mm " . format ( shortPath [ 0 ] . get ( ' id ' ) , round ( self . svg . uutounit ( str ( shortPath [ 1 ] ) , " mm " ) , 3 ) ) )
2021-10-23 22:42:22 +02:00
2021-11-02 20:34:09 +01:00
'''
Really short paths can cause issues with laser cutter mechanics and should be avoided to
have healthier stepper motor belts , etc .
'''
if so . checks == " check_all " or so . cutting_estimation is True :
2021-11-07 05:33:59 +01:00
inkex . utils . debug ( " \n ---------- Cutting time estimation (Epilog Lasers) " )
2021-11-03 03:52:37 +01:00
totalCuttingLength = 0
2021-11-07 05:33:59 +01:00
pathCount = 0
2021-11-02 20:34:09 +01:00
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
slengths , stotal = csplength ( element . path . transform ( element . composed_transform ( ) ) . to_superpath ( ) )
2021-11-03 03:52:37 +01:00
totalCuttingLength + = stotal
2021-11-07 05:33:59 +01:00
pathCount + = 1 #each path has one start and one end (if open path) or start=end on closed path. For cutting the we calculate with 2 points because we enter and leave each path ALWAYS
totalTravelLength = totalCuttingLength * so . cut_travel_factor
2021-11-03 03:52:37 +01:00
totalLength = totalCuttingLength + totalTravelLength
2021-11-07 05:33:59 +01:00
inkex . utils . debug ( " total paths= {} " . format ( pathCount ) )
2021-11-03 03:52:37 +01:00
inkex . utils . debug ( " (measured) cutting length (mm) = {:0.2f} mm " . format ( self . svg . uutounit ( str ( totalCuttingLength ) , " mm " ) , self . svg . uutounit ( str ( totalCuttingLength ) , " mm " ) ) )
inkex . utils . debug ( " (estimated) travel length (mm) = {:0.2f} mm " . format ( self . svg . uutounit ( str ( totalTravelLength ) , " mm " ) , self . svg . uutounit ( str ( totalTravelLength ) , " mm " ) ) )
inkex . utils . debug ( " (estimated) total length (mm) = {:0.2f} mm " . format ( self . svg . uutounit ( str ( totalLength ) , " mm " ) , self . svg . uutounit ( str ( totalLength ) , " mm " ) ) )
2021-11-07 05:33:59 +01:00
''' from https://www.epiloglaser.com/assets/downloads/fusion-material-settings.pdf
Speed Settings : The speed setting scale of 1 % to 100 % is not linear –
i . e . 100 % speed will not be twice as fast as 50 % speed . This non - linear
scale is very useful in compensating for the different factors that affect engraving time .
'''
for speedFactor in [ 100 , 90 , 80 , 70 , 60 , 50 , 40 , 30 , 20 , 10 , 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 ] :
speedFactorR = speedFactor / 100.0
adjusted_speed = 482.4000 / so . max_cutting_speed #empiric - found out by trying for hours ...
empiric_scale = 1 + ( speedFactorR * * 2 ) / 19.0 #empiric - found out by trying for hours ...
v_cut = so . max_cutting_speed * speedFactorR
2021-11-03 03:52:37 +01:00
v_travel = so . max_travel_speed #this is always at maximum
2021-11-07 05:33:59 +01:00
tsec_cut = ( self . svg . uutounit ( str ( totalCuttingLength ) ) / ( adjusted_speed * so . max_cutting_speed * speedFactorR ) ) * empiric_scale
2021-11-03 03:52:37 +01:00
tsec_travel = self . svg . uutounit ( str ( totalTravelLength ) ) / v_travel
tsec_total = tsec_cut + tsec_travel
minutes , seconds = divmod ( tsec_total , 60 ) # split the seconds to minutes and seconds
partial_minutes = round ( seconds / 60 * 2 ) / 2
inkex . utils . debug ( " @ {:03.0f} % (cut= {:06.2f} mm/s | travel= {:06.2f} mm/s) > {:03.0f} min {:02.0f} sec | cost= {:02.0f} € " . format ( speedFactor , v_cut , v_travel , minutes , seconds , so . price_per_minute_gross * ( minutes + partial_minutes ) ) )
2021-11-02 02:41:57 +01:00
2021-11-07 05:33:59 +01:00
''' Measurements from Epilog Software Suite
We are using a huge SVG graphic with 100 meters ( = 100.000 mm ) of lines .
The following speeds are getting precalculated ( travel moves = 0 mm ) :
@ 100 % = 13 : 45 = 825 s - > 121 , 21 mm / s
@ 090 % = 15 : 12 = 912 s - > 109 , 65 mm / s
@ 080 % = 17 : 01 = 1021 s - > 97 , 94 mm / s
@ 070 % = 19 : 21 = 1161 s - > 86 , 13 mm / s
@ 060 % = 22 : 28 = 1348 s - > 74 , 18 mm / s
@ 050 % = 26 : 49 = 1609 s - > 62 , 15 mm / s
@ 040 % = 33 : 21 = 2001 s - > 49 , 98 mm / s
@ 030 % = 44 : 13 = 2653 s - > 37 , 69 mm / s
@ 020 % = 65 : 51 = 3951 s - > 25 , 31 mm / s
@ 010 % = 130 : 52 = 7852 s - > 12 , 74 mm / s
@ 009 % = 145 : 21 = 8721 s - > 11 , 47 mm / s
@ 008 % = 163 : 27 = 9807 s - > 10 , 20 mm / s
@ 007 % = 186 : 44 = 11204 s - > 8 , 93 mm / s
@ 006 % = 217 : 48 = 13068 s - > 7 , 65 mm / s
@ 005 % = 261 : 18 = 15678 s - > 6 , 38 mm / s
@ 004 % = 326 : 34 = 19594 s - > 5 , 10 mm / s
@ 003 % = 435 : 21 = 26121 s - > 3 , 83 mm / s
@ 002 % = 652 : 57 = 39177 s - > 2 , 55 mm / s
@ 001 % = 1305 : 49 = 78349 s - > 1 , 28 mm / s
'''
2021-11-02 02:41:57 +01:00
'''
Paths with a high amount of nodes will cause issues because each node means slowing down / speeding up the laser mechanics
'''
if so . checks == " check_all " or so . nodes_per_path is True :
inkex . utils . debug ( " \n ---------- Heavy node-loaded paths (allowed: {} node(s) per {} mm) - should be simplified " . format ( so . nodes_per_path_max , round ( so . nodes_per_path_interval , 3 ) ) )
heavyPaths = [ ]
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
slengths , stotal = csplength ( element . path . transform ( element . composed_transform ( ) ) . to_superpath ( ) )
nodes = len ( element . path )
if nodes / stotal > so . nodes_per_path_max / self . svg . unittouu ( str ( so . nodes_per_path_interval ) + " mm " ) :
heavyPaths . append ( [ element , nodes , stotal ] )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 02:41:57 +01:00
inkex . utils . debug ( " {} Heavy node-loaded paths in total " . format ( len ( heavyPaths ) ) )
for heavyPath in heavyPaths :
inkex . utils . debug ( " id= {} , nodes= {} , length= {} mm, density= {} nodes/mm " . format (
heavyPath [ 0 ] . get ( ' id ' ) ,
heavyPath [ 1 ] ,
round ( self . svg . uutounit ( str ( heavyPath [ 2 ] ) , " mm " ) , 3 ) ,
round ( heavyPath [ 1 ] / self . svg . uutounit ( str ( heavyPath [ 2 ] ) , " mm " ) , 3 )
)
2021-11-02 19:28:26 +01:00
)
2021-11-02 02:41:57 +01:00
2021-11-02 19:28:26 +01:00
'''
2021-11-03 03:52:37 +01:00
Elements outside canvas or touching the border . These are critical because they won ' t be lasered or not correctly lasered
2021-11-02 19:28:26 +01:00
'''
if so . checks == " check_all " or so . elements_outside_canvas is True :
inkex . utils . debug ( " \n ---------- Elements outside canvas or touching the border " )
elementsOutside = [ ]
for element in shapes :
if element . tag != inkex . addNS ( ' g ' , ' svg ' ) :
ebbox = element . bounding_box ( )
precision = 3
#inkex.utils.debug("{} | bbox: left = {:0.3f} right = {:0.3f} top = {:0.3f} bottom = {:0.3f}".format(element.get('id'), ebbox.left, ebbox.right, ebbox.top, ebbox.bottom))
pagew = round ( self . svg . unittouu ( self . svg . get ( ' width ' ) ) , precision )
pageh = round ( self . svg . unittouu ( self . svg . get ( ' height ' ) ) , precision )
if round ( ebbox . right , precision ) == 0 or \
round ( ebbox . left , precision ) == pagew or \
round ( ebbox . top , precision ) == 0 or \
round ( ebbox . bottom , precision ) == pageh :
elementsOutside . append ( [ element , " touching " ] )
elif \
round ( ebbox . right , precision ) < 0 or \
round ( ebbox . left , precision ) > pagew or \
round ( ebbox . top , precision ) < 0 or \
round ( ebbox . bottom , precision ) > pageh :
elementsOutside . append ( [ element , " fully outside " ] )
else : #fully inside or partially inside/outside. we check if one or more corners is outside the canvas
rightOutside = False
leftOutside = False
topOutside = False
bottomOutside = False
if round ( ebbox . right , precision ) < 0 or round ( ebbox . right , precision ) > pagew :
rightOutside = True
if round ( ebbox . left , precision ) < 0 or round ( ebbox . left , precision ) > pagew :
leftOutside = True
if round ( ebbox . top , precision ) < 0 or round ( ebbox . top , precision ) > pageh :
topOutside = True
if round ( ebbox . bottom , precision ) < 0 or round ( ebbox . bottom , precision ) > pageh :
bottomOutside = True
if rightOutside is True or leftOutside is True or topOutside is True or bottomOutside is True :
elementsOutside . append ( [ element , " partially outside " ] )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-02 19:28:26 +01:00
inkex . utils . debug ( " {} Elements outside canvas or touching the border in total " . format ( len ( elementsOutside ) ) )
for elementOutside in elementsOutside :
inkex . utils . debug ( " id= {} , status= {} " . format (
elementOutside [ 0 ] . get ( ' id ' ) ,
elementOutside [ 1 ]
)
)
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
Shapes like rectangles , ellipses , arcs , spirals should be converted to svg : path to have more
convenience in the file
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . non_path_shapes is True :
inkex . utils . debug ( " \n ---------- Non-path shapes - should be converted to paths " )
nonPathShapes = [ ]
for element in shapes :
if not isinstance ( element , inkex . PathElement ) and not isinstance ( element , inkex . Group ) :
nonPathShapes . append ( element )
2021-11-03 03:52:37 +01:00
if so . show_issues_only is False :
2021-11-01 23:31:07 +01:00
inkex . utils . debug ( " {} non-path shapes in total " . format ( len ( nonPathShapes ) ) )
2021-10-23 22:42:22 +02:00
for nonPathShape in nonPathShapes :
inkex . utils . debug ( " id= {} " . format ( nonPathShape . get ( ' id ' ) ) )
2021-10-23 23:10:32 +02:00
exit ( 0 )
2021-10-23 02:32:35 +02:00
if __name__ == ' __main__ ' :
LaserCheck ( ) . run ( )