1275 lines
72 KiB
Python
1275 lines
72 KiB
Python
#!/usr/bin/env python3
|
||
|
||
import inkex
|
||
from inkex.bezier import csplength, csparea
|
||
from lxml import etree
|
||
import re
|
||
import math
|
||
import sys
|
||
from math import log
|
||
import datetime
|
||
import os
|
||
from collections import Counter
|
||
from PIL import Image
|
||
from io import BytesIO
|
||
import base64
|
||
import urllib.request as urllib
|
||
from _ast import Or
|
||
|
||
class LaserCheck(inkex.EffectExtension):
|
||
|
||
'''
|
||
ToDos:
|
||
- inx:
|
||
- set speed manually or pick machine (epilog) - travel and cut speed are prefilled then
|
||
- calculate cut estimation with linear or non-linear (epilog) speeds > select formula or like this
|
||
- select material (parameters -> how to???)
|
||
- add fields for additional costs like configuring the machine or grabbing parts out of the machine (weeding), etc.
|
||
- add mode select: cut, engrave
|
||
- visualize results as nice markdown formatted file
|
||
- run as script to generate quick results for users
|
||
- check for old styles which should be upgraded (cleanup styles tool)
|
||
- check for elements which have no style attribute (should be created) -> (cleanup styles tool)
|
||
- self-intersecting paths
|
||
- number of parts (isles) to weed in total - this is an indicator for manually picking work; if we add bridges we have less work
|
||
- number of parts which are smaller than vector grid (which call fall off when lasercutting)
|
||
- 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
|
||
'''
|
||
|
||
def checkImagePath(self, node):
|
||
"""Embed the data of the selected Image Tag element"""
|
||
xlink = node.get('xlink:href')
|
||
if xlink and xlink[:5] == 'data:':
|
||
# No need, data alread embedded
|
||
return
|
||
|
||
url = urllib.urlparse(xlink)
|
||
href = urllib.url2pathname(url.path)
|
||
|
||
# Primary location always the filename itself.
|
||
path = self.absolute_href(href or '')
|
||
|
||
# Backup directory where we can find the image
|
||
if not os.path.isfile(path):
|
||
path = node.get('sodipodi:absref', path)
|
||
|
||
if not os.path.isfile(path):
|
||
inkex.errormsg('File not found "{}". Unable to embed image.').format(path)
|
||
return
|
||
|
||
if (os.path.isfile(path)):
|
||
return path
|
||
|
||
def add_arguments(self, pars):
|
||
pars.add_argument('--tab')
|
||
|
||
pars.add_argument('--machine_size', default="812x508")
|
||
pars.add_argument('--max_cutting_speed', type=float, default=120.0)
|
||
pars.add_argument('--max_travel_speed', type=float, default=450.0)
|
||
pars.add_argument('--job_time_offset', type=float, default=0.0)
|
||
pars.add_argument('--price_per_minute_gross', type=float, default=2.0)
|
||
pars.add_argument('--vector_grid_xy', type=float, default=12.0) #TODO
|
||
pars.add_argument('--co2_power', type=float, default=60.0) #TODO
|
||
pars.add_argument('--round_times', type=inkex.Boolean, default=True)
|
||
|
||
pars.add_argument('--show_issues_only', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--show_expert_tips', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--checks', default="check_all")
|
||
pars.add_argument('--basic_checks', type=inkex.Boolean, default=True)
|
||
pars.add_argument('--filesize_max', type=float, default=2048.000)
|
||
pars.add_argument('--bbox', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--bbox_offset', type=float, default=5.000)
|
||
pars.add_argument('--cutting_estimation', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--cutting_speedfactors', default="100 90 80 70 60 50 40 30 20 10 9 8 7 6 5 4 3 2 1")
|
||
pars.add_argument('--elements_outside_canvas', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--groups_and_layers', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--nest_depth_max', type=int, default=2)
|
||
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('--min_image_dpi', type=int, default=300)
|
||
pars.add_argument('--max_image_dpi', type=int, default=1200)
|
||
pars.add_argument('--texts', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--filters', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--lowlevelstrokes', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--style_types', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--stroke_colors', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--stroke_colors_max', type=int, default=3)
|
||
pars.add_argument('--stroke_widths', type=inkex.Boolean, default=False)
|
||
pars.add_argument('--stroke_widths_max', type=int, default=1)
|
||
pars.add_argument('--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('--combined_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)
|
||
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)
|
||
|
||
def effect(self):
|
||
|
||
so = self.options
|
||
docroot = self.document.getroot()
|
||
|
||
machineWidth = self.svg.unittouu(so.machine_size.split('x')[0] + "mm")
|
||
machineHeight = self.svg.unittouu(so.machine_size.split('x')[1] + "mm")
|
||
selected = [] #total list of elements to parse
|
||
|
||
|
||
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)
|
||
parseChildren(child) #go deeper and deeper
|
||
|
||
#check if we have selected elements or if we should parse the whole document instead
|
||
if len(self.svg.selected) == 0:
|
||
for element in docroot.iter(tag=etree.Element):
|
||
if element != docroot:
|
||
|
||
selected.append(element)
|
||
else:
|
||
for element in self.svg.selected.values():
|
||
parseChildren(element)
|
||
|
||
namedView = docroot.find(inkex.addNS('namedview', 'sodipodi'))
|
||
doc_units = namedView.get(inkex.addNS('document-units', 'inkscape'))
|
||
user_units = namedView.get(inkex.addNS('units'))
|
||
pagecolor = namedView.get('pagecolor')
|
||
|
||
'''
|
||
Check for scalings
|
||
> Page size is determined by SVG root 'width' and 'height'.
|
||
> 'viewBox' defined in 'user units' with the values: (x offset, y-offset, width, height).
|
||
> Document scale is determined by ratio of 'width'/'height' to 'viewBox'.
|
||
'''
|
||
|
||
inkscapeScale = self.svg.inkscape_scale #this is the "Scale:" value at "Display tab"
|
||
#docScale = self.svg.scale
|
||
#docWidth = self.svg.viewport_width
|
||
#docHeight = self.svg.viewport_height
|
||
#inkex.utils.debug("Document scale (x/y)={:0.3f}".format(docScale))
|
||
#inkex.utils.debug("Document width={:0.3f}".format(docWidth))
|
||
vxMin, vyMin, vxMax, vyMax = self.svg.get_viewbox()
|
||
#vxTotal = vxMax - vxMin
|
||
#vyTotal = vyMax - vyMin
|
||
#vScaleX = self.svg.unittouu(str(vxTotal / docWidth) + doc_units)
|
||
#vScaleXpx = self.svg.unittouu(str(vxTotal / docWidth) + "px")
|
||
#vScaleY = vyTotal / docHeight #should/must be the same as vScaleX value
|
||
#inkex.utils.debug(vxTotal)
|
||
#inkex.utils.debug(vyTotal)
|
||
#inkex.utils.debug(vScaleY)
|
||
#inkex.utils.debug("Document scale (x/y): {:0.5f}{} ({:0.5f}px)".format(vScaleX, doc_units, vScaleXpx))
|
||
#if round(vScaleX, 5) != 1.0:
|
||
# inkex.utils.debug("WARNING: Document scale not 100%!")
|
||
scaleOk = True
|
||
if round(inkscapeScale, 5) != 1.0:
|
||
scaleOk = False
|
||
scaleX = namedView.get('scale-x')
|
||
viewboxOk = True
|
||
if vxMin < 0 or vyMin < 0 or vxMax < 0 or vyMax < 0:
|
||
viewboxOk = False
|
||
|
||
'''
|
||
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, etc.
|
||
'''
|
||
nonShapes = []
|
||
shapes = [] #this may contains paths, rectangles, circles, groups and more
|
||
for element in selected:
|
||
if not isinstance(element, inkex.ShapeElement):
|
||
if element.tag not in (
|
||
"{http://www.w3.org/2000/svg}defs",
|
||
"{http://www.w3.org/2000/svg}metadata",
|
||
"{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview",
|
||
"{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF",
|
||
"{http://creativecommons.org/ns#}Work"):
|
||
nonShapes.append(element)
|
||
else:
|
||
shapes.append(element)
|
||
|
||
elementTypes = []
|
||
for element in selected:
|
||
if element not in elementTypes:
|
||
elementTypes.append(element.tag
|
||
.replace("{http://www.w3.org/2000/svg}", "")
|
||
.replace("{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}", "")
|
||
.replace("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}", "")
|
||
.replace("{http://creativecommons.org/ns#}", "")
|
||
.replace("{http://www.inkscape.org/namespaces/inkscape}", "")
|
||
)
|
||
|
||
counter = Counter(elementTypes)
|
||
uniqElementTypes = counter
|
||
|
||
if so.basic_checks is True:
|
||
inkex.utils.debug("---------- Default checks")
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("Document units: {}".format(doc_units))
|
||
inkex.utils.debug("User units: {}".format(user_units))
|
||
inkex.utils.debug("Document scale (x/y): {:0.5f}".format(inkscapeScale))
|
||
if scaleOk is False:
|
||
inkex.utils.debug("WARNING: Document scale not 100%!")
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("EXTENSION TIP:\n"\
|
||
" - Use 'FabLab Chemnitz > Transformations > Normalize Drawing Scale' can fix this")
|
||
if scaleX is not None:
|
||
inkex.utils.debug("WARNING: Document has scale-x attribute with value={}".format(scaleX))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("Viewbox:\n x.min = {:0.0f}\n y.min = {:0.0f}\n x.max = {:0.0f}\n y.max = {:0.0f}".format( vxMin, vyMin, vxMax, vyMax))
|
||
if viewboxOk is False:
|
||
# values may be lower than 0, but it does not make sense. The viewbox defines the top-left corner, which is usually 0,0. In case we want to allow that, we need to convert all bounding boxes accordingly. See also https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox.
|
||
inkex.utils.debug("WARNING: viewBox does not start at 0,0. Visible results will differ from real coordinates (shifting).")
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("EXTENSION TIP:\n"\
|
||
" - Use 'FabLab Chemnitz > Transformations > Normalize Drawing Scale' can fix this")
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} shape elements in total".format(len(shapes)))
|
||
inkex.utils.debug("{} non-shape elements in total".format(len(nonShapes)))
|
||
for nonShape in nonShapes:
|
||
inkex.utils.debug("non-shape id={}".format(nonShape.get('id')))
|
||
#that size is actually not the stored one on file system
|
||
#filesize = len(etree.tostring(self.document, pretty_print=True).decode('UTF-8')) / 1000
|
||
filesize = os.path.getsize(so.input_file) / 1000
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("File size: {:0.1f} KB".format(filesize))
|
||
if filesize > so.filesize_max:
|
||
inkex.utils.debug("WARNING: file size is larger than allowed: {} KB > {} KB".format(filesize, so.filesize_max))
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("SOME TIPS TO REDUCE FILE SIZE:\n"\
|
||
" - Reduce count of nodes on paths / reduce duplicate segments / remove useless:\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Contour Scanner and Trimmer'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Duplicate Path Nodes'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Duplicate Path Segments'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Remove Duplicate Line Segments'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Pointy Paths'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Filter by Length/Area'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Join/Order > Chain Paths'\n"\
|
||
" - Simplify by CTRL+L\n"\
|
||
" - Merge (combine) paths\n"\
|
||
" - Cut off decimals (over-precision) from path data:\n"\
|
||
" - Use 'FabLab Chemnitz > Modify Existing Path(s) > Rounder'\n"\
|
||
" - Purge other unrequirements elements from SVG tree:\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Remove Empty Groups'\n"
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Ungrouper and Element Migrator/Filter'\n"
|
||
)
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("Total overview of element types:")
|
||
for key in counter.keys():
|
||
inkex.utils.debug(" - {}: {}x".format(key, counter[key]))
|
||
|
||
|
||
'''
|
||
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))
|
||
if scaleOk is False:
|
||
inkex.utils.debug("WARNING: Document scale is not 100%. Calculating bounding boxes might create wrong results.")
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("EXTENSION TIP:\n"\
|
||
" - Use 'FabLab Chemnitz > Transformations > Normalize Drawing Scale' can fix this")
|
||
if viewboxOk is False:
|
||
inkex.utils.debug("WARNING: Viewbox does not start at 0,0. Calculating bounding boxes might create wrong results.")
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("EXTENSION TIP:\n"\
|
||
" - Use 'FabLab Chemnitz > Transformations > Normalize Drawing Scale' can fix this")
|
||
bbox = inkex.BoundingBox()
|
||
for element in selected:
|
||
#for element in docroot.iter(tag=etree.Element):
|
||
if element != docroot 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.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))
|
||
elif so.show_issues_only is False:
|
||
inkex.utils.debug("bounding box is:\n x.min = {}\n y.min = {}\n x.max = {}\n y.max = {}".format(bbox.left, bbox.top, bbox.right, bbox.bottom))
|
||
page_width = self.svg.unittouu(docroot.attrib['width'])
|
||
width_height = self.svg.unittouu(docroot.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:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("left border... ok")
|
||
else:
|
||
inkex.utils.debug("left border... fail: {:0.3f} mm".format(self.svg.uutounit(bb_left, "mm")))
|
||
|
||
if bb_top >= fmm:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("top border... ok")
|
||
else:
|
||
inkex.utils.debug("top border... fail: {:0.3f} mm".format(self.svg.uutounit(bb_top, "mm")))
|
||
|
||
if bb_right + fmm <= page_width:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("right border... ok")
|
||
else:
|
||
inkex.utils.debug("right border... fail: {:0.3f} mm".format(self.svg.uutounit(bb_right, "mm")))
|
||
|
||
if bb_bottom + fmm <= width_height:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("bottom border... ok")
|
||
else:
|
||
inkex.utils.debug("bottom border... fail: {:0.3f} mm".format(self.svg.uutounit(bb_bottom, "mm")))
|
||
if bb_width <= machineWidth:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("page width... ok")
|
||
else:
|
||
inkex.utils.debug("page width... fail: {:0.3f} mm".format(bb_width))
|
||
if bb_height <= machineHeight:
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("page height... ok")
|
||
else:
|
||
inkex.utils.debug("page height... fail: {:0.3f} mm".format(bb_height))
|
||
|
||
|
||
'''
|
||
We check for possible deep nested groups/layers, empty groups/layers or groups/layers with styles.
|
||
'''
|
||
if so.checks == "check_all" or so.groups_and_layers is True:
|
||
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(docroot, -1)
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("Maximum group depth={}".format(md - 1))
|
||
if md - 1 > so.nest_depth_max:
|
||
inkex.utils.debug("Warning: maximum allowed group depth reached: {}".format(so.nest_depth_max))
|
||
groups = []
|
||
emptyGroups = 0
|
||
layers = []
|
||
emptyLayers = 0
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('g','svg'):
|
||
if element.get('inkscape:groupmode') == 'layer':
|
||
layers.append(element)
|
||
else:
|
||
groups.append(element)
|
||
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} groups in total".format(len(groups)))
|
||
inkex.utils.debug("{} layers in total".format(len(layers)))
|
||
|
||
#check for empty groups
|
||
for group in groups:
|
||
if len(group) == 0:
|
||
emptyGroups += 1
|
||
inkex.utils.debug("id={} is empty group".format(group.get('id')))
|
||
|
||
#check for empty layers
|
||
for layer in layers:
|
||
if len(layer) == 0:
|
||
emptyLayers += 1
|
||
inkex.utils.debug("id={} is empty layer".format(layer.get('id')))
|
||
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} empty groups total".format(emptyGroups))
|
||
inkex.utils.debug("{} empty layers total".format(emptyLayers))
|
||
if so.show_expert_tips is True and (emptyGroups > 0 or emptyLayers > 0):
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Remove Empty Groups'\n"
|
||
)
|
||
|
||
'''
|
||
Style scheme in svg. We can style elements by ...
|
||
- "style" attribute for elements like svg:path
|
||
- dedicated attributes for elements like svg:path
|
||
- "style" attributes or dedicated attributes at group level
|
||
- css class together with svg:style elements
|
||
For a cleaner file we should avoid to mess up. Best is to define styles
|
||
at svg:path level or using properly defined css classes
|
||
'''
|
||
if so.checks == "check_all" or so.style_types is True:
|
||
inkex.utils.debug("\n---------- Style types")
|
||
groupStyles = []
|
||
svgStyleElements = []
|
||
styleInNonGroupLayerShapes = []
|
||
dedicatedStylesInNonGroupLayerShapes = []
|
||
dedicatedStyleDict = []
|
||
dedicatedStyleDict.extend(['opacity', 'stroke', 'stroke-opacity', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'fill', 'fill-opacity'])
|
||
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('g','svg'):
|
||
if element.style is not None and element.style != "": #style may also be just empty (weird, but was validated on 21.12.2021)
|
||
groupStyles.append(element)
|
||
if element.tag == inkex.addNS('style', 'svg'):
|
||
svgStyleElements.append(element)
|
||
for element in shapes:
|
||
if element.tag != inkex.addNS('g','svg'):
|
||
if element.get('style') is not None: #do not use "element.style" - this uses the composed style from parent
|
||
styleInNonGroupLayerShapes.append(element)
|
||
for dedicatedStyleItem in dedicatedStyleDict:
|
||
if element.attrib.has_key(str(dedicatedStyleItem)):
|
||
dedicatedStylesInNonGroupLayerShapes.append(element)
|
||
for groupStyle in groupStyles:
|
||
inkex.utils.debug("group id={} has style attribute".format(groupStyle.get('id')))
|
||
for svgStyleElement in svgStyleElements:
|
||
inkex.utils.debug("id={} is svg:style element".format(svgStyleElement.get('id')))
|
||
for styleInNonGroupLayerShape in styleInNonGroupLayerShapes:
|
||
inkex.utils.debug("shape id={} has style attribute".format(styleInNonGroupLayerShape.get('id')))
|
||
for dedicatedStylesInNonGroupLayerShape in dedicatedStylesInNonGroupLayerShapes:
|
||
inkex.utils.debug("shape id={} uses dedicated style attribute(s)".format(dedicatedStylesInNonGroupLayerShape.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} groups/layers with style attribute in total".format(len(groupStyles)))
|
||
inkex.utils.debug("{} svg:style elements in total".format(len(svgStyleElements)))
|
||
inkex.utils.debug("{} shapes using style attribute in total".format(len(styleInNonGroupLayerShapes)))
|
||
inkex.utils.debug("{} shapes using dedicated style attributes in total".format(len(dedicatedStylesInNonGroupLayerShapes)))
|
||
if so.show_expert_tips is True:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Colors/Gradients/Filters > Cleanup Styles'\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Styles to Layers'"
|
||
)
|
||
|
||
'''
|
||
Clones should be unlinked because they cause similar issues like transformations
|
||
'''
|
||
if so.checks == "check_all" or so.clones is True:
|
||
inkex.utils.debug("\n---------- Clones (svg:use)")
|
||
uses = []
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('use','svg'):
|
||
uses.append(element)
|
||
for use in uses:
|
||
inkex.utils.debug("id={}".format(use.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} svg:use clones in total".format(len(uses)))
|
||
if so.show_expert_tips is True and len(uses) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Unlink Clones to make them unique objects")
|
||
|
||
'''
|
||
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.
|
||
'''
|
||
if so.checks == "check_all" or so.clippaths is True:
|
||
inkex.utils.debug("\n---------- Clippings (svg:clipPath)")
|
||
clipPaths = []
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('clipPath','svg'):
|
||
clipPaths.append(element)
|
||
for clipPath in clipPaths:
|
||
inkex.utils.debug("id={}".format(clipPath.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} svg:clipPath in total".format(len(clipPaths)))
|
||
if so.show_expert_tips is True and len(clipPaths) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Replace clipped paths with real cutting paths")
|
||
|
||
'''
|
||
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
|
||
'''
|
||
if so.checks == "check_all" or so.images is True:
|
||
inkex.utils.debug("\n---------- Images (svg:image)")
|
||
images = []
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('image','svg'):
|
||
images.append(element)
|
||
malformedScales = []
|
||
maxDPIhits = []
|
||
minDPIhits = []
|
||
|
||
for image in images:
|
||
self.path = self.checkImagePath(image) # This also ensures the file exists
|
||
if self.path is None: # check if image is embedded or linked
|
||
image_string = image.get('{http://www.w3.org/1999/xlink}href')
|
||
# find comma position
|
||
i = 0
|
||
while i < 40:
|
||
if image_string[i] == ',':
|
||
break
|
||
i = i + 1
|
||
img = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)])))
|
||
else:
|
||
img = Image.open(self.path)
|
||
|
||
img_w = img.getbbox()[2]
|
||
img_h = img.getbbox()[3]
|
||
if image.get('width') is None:
|
||
img_svg_w = img_w * inkscapeScale
|
||
else:
|
||
try:
|
||
img_svg_w = float(image.get('width')) * inkscapeScale
|
||
except Exception as e:
|
||
img_svg_w = self.svg.unittouu(image.get('width')) * inkscapeScale
|
||
if image.get('height') is None:
|
||
img_svg_h = img_h * inkscapeScale
|
||
else:
|
||
try:
|
||
img_svg_h = float(image.get('height')) * inkscapeScale
|
||
except Exception as e:
|
||
img_svg_h = self.svg.unittouu(image.get('height')) * inkscapeScale
|
||
|
||
imgScaleX = img_svg_w / img_w
|
||
imgScaleY = img_svg_h / img_h
|
||
uniform = False #check for aspect ratio
|
||
if round(imgScaleX, 3) == round(imgScaleY, 3) or \
|
||
image.get('preserveAspectRatio') is None or \
|
||
"none" not in image.get('preserveAspectRatio'):
|
||
uniform = True
|
||
else:
|
||
malformedScales.append([image, imgScaleX, imgScaleY])
|
||
|
||
dpiX = self.svg.unittouu(str(img_w) + "in") / img_svg_w
|
||
dpiY = self.svg.unittouu(str(img_h) + "in") / img_svg_h
|
||
|
||
if round(dpiX, 0) < so.min_image_dpi or (round(dpiY, 0) < so.min_image_dpi and uniform is False):
|
||
minDPIhits.append([image, dpiX, dpiY, img_svg_w, img_svg_h])
|
||
if round(dpiX, 0) > so.max_image_dpi or (round(dpiY, 0) > so.max_image_dpi and uniform is False):
|
||
maxDPIhits.append([image, dpiX, dpiY, img_svg_w, img_svg_h])
|
||
|
||
inkex.utils.debug("image id={} - internal size {}x{}px - scaled to {:0.2f}x{:0.2f}px - DPI X{:0.2f}+Y{:0.2f} - uniform = {}".format(image.get('id'), img_w, img_h, img_svg_w, img_h*img_svg_w/img_w if uniform is True else img_svg_h, dpiX, dpiX if uniform is True else dpiY, uniform))
|
||
|
||
dpi_string = "Image {} has DPI X{:0.2f}+Y{:0.2f} {} {:0.0f} ({}). "\
|
||
"Resize to {:0.2f}x{:0.2f}px to fit or try to set/change preserveAspectRatio attribute"
|
||
if len(minDPIhits) > 0:
|
||
for minDPIhit in minDPIhits:
|
||
inkex.utils.debug(dpi_string.format(
|
||
minDPIhit[0].get('id'),
|
||
minDPIhit[1],
|
||
minDPIhit[2],
|
||
"<",
|
||
so.min_image_dpi,
|
||
"minimum",
|
||
minDPIhit[3] * (minDPIhit[1] / so.min_image_dpi),
|
||
minDPIhit[4] * (minDPIhit[2] / so.min_image_dpi),
|
||
))
|
||
if len(maxDPIhits) > 0:
|
||
for maxDPIhit in maxDPIhits:
|
||
inkex.utils.debug(dpi_string.format(
|
||
maxDPIhit[0].get('id'),
|
||
maxDPIhit[1],
|
||
maxDPIhit[2],
|
||
">",
|
||
so.max_image_dpi,
|
||
"maximum",
|
||
maxDPIhit[3] * (maxDPIhit[1] / so.max_image_dpi) * inkscapeScale,
|
||
maxDPIhit[4] * (maxDPIhit[2] / so.max_image_dpi) * inkscapeScale,
|
||
))
|
||
if len(malformedScales) > 0:
|
||
for malformedScale in malformedScales:
|
||
inkex.utils.debug("Image {} has non-uniform scale X = {:0.3f}, Y = {:0.3f}".format(malformedScale[0].get('id'), malformedScale[1], malformedScale[2]))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} svg:image in total".format(len(images)))
|
||
if so.show_expert_tips is True and len(images) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Maybe trace images using built-in image tracer: ALT + SHIFT + B or use Imagetracer.js or KVEC")
|
||
|
||
|
||
'''
|
||
Low level strokes cannot be properly edited in Inkscape (no node handles). Converting helps
|
||
'''
|
||
if so.checks == "check_all" or so.lowlevelstrokes is True:
|
||
inkex.utils.debug("\n---------- Low level strokes (svg:line/polyline/polygon)")
|
||
lowlevels = []
|
||
for element in selected:
|
||
if element.tag in (inkex.addNS('line','svg'), inkex.addNS('polyline','svg'), inkex.addNS('polygon','svg')):
|
||
lowlevels.append(element)
|
||
for lowlevel in lowlevels:
|
||
inkex.utils.debug("id={}".format(lowlevel.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} low level strokes in total".format(len(lowlevels)))
|
||
if so.show_expert_tips is True and len(lowlevels) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Convert low level strokes like svg:line, svg:polyline and svg:polygon to path")
|
||
|
||
'''
|
||
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.
|
||
'''
|
||
if so.checks == "check_all" or so.texts is True:
|
||
inkex.utils.debug("\n---------- Texts")
|
||
texts = []
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('text','svg'):
|
||
texts.append(element)
|
||
for text in texts:
|
||
inkex.utils.debug("id={}".format(text.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} svg:text in total".format(len(texts)))
|
||
if so.show_expert_tips is True and len(texts) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Convert text elements to paths. So we do not require the font file to be installed at target system")
|
||
|
||
'''
|
||
Filters on elements let Epilog Software Suite always think vectors should get to raster image data. That might be good sometimes,
|
||
but not in usual case.
|
||
'''
|
||
if so.checks == "check_all" or so.filters is True:
|
||
inkex.utils.debug("\n---------- Filters")
|
||
|
||
filter_elements = []
|
||
for element in selected:
|
||
if element.tag == inkex.addNS('filter','svg'):
|
||
filter_elements.append(element)
|
||
filter_styles = []
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} filters (as svg:filter) in total".format(len(filter_elements)))
|
||
for filter_element in filter_elements:
|
||
inkex.utils.debug("id={}".format(filter_element.get('id')))
|
||
|
||
for element in selected:
|
||
filter_style = [element, element.style.get('filter')]
|
||
if filter_style[1] is None or filter_style[1] == "none":
|
||
filter_style[1] = "none"
|
||
if filter_style[1] != "none" and filter_style not in filter_styles:
|
||
filter_styles.append(filter_style)
|
||
for filter_style in filter_styles:
|
||
inkex.utils.debug("id={}, filter={}".format(filter_style[0].get('id'), filter_style[1]))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} filters (in styles) in total".format(len(filter_styles)))
|
||
if so.show_expert_tips is True and len(filter_styles) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Ungrouper and Element Migrator/Filter'")
|
||
|
||
'''
|
||
The more stroke colors the more laser job configuration is required. Reduce the SVG file
|
||
to a minimum of stroke colors to be quicker. Note that a None stroke might be same like #000000 but thats not guaranteed
|
||
'''
|
||
if so.checks == "check_all" or so.stroke_colors is True:
|
||
inkex.utils.debug("\n---------- Stroke colors ({} are allowed)".format(so.stroke_colors_max))
|
||
strokeColors = []
|
||
for element in shapes:
|
||
strokeColor = element.style.get('stroke')
|
||
if strokeColor not in strokeColors: #we also add None (default value is #000000 then) and "none" values.
|
||
strokeColors.append(strokeColor)
|
||
if len(strokeColors) > so.stroke_colors_max:
|
||
for strokeColor in strokeColors:
|
||
inkex.utils.debug("stroke color {}".format(strokeColor))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} different stroke colors in total".format(len(strokeColors)))
|
||
if so.show_expert_tips is True and len(strokeColors) > so.stroke_colors_max:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Colors/Gradients/Filters > Cleanup Styles'\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Styles to Layers'"
|
||
)
|
||
|
||
|
||
'''
|
||
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.
|
||
'''
|
||
if so.checks == "check_all" or so.stroke_widths is True:
|
||
inkex.utils.debug("\n---------- Stroke widths ({} are allowed)".format(so.stroke_widths_max))
|
||
strokeWidths = []
|
||
for element in shapes:
|
||
strokeWidth = element.style.get('stroke-width')
|
||
if strokeWidth not in strokeWidths: #we also add None and "none" values. Default width for None value seems to be 1px
|
||
strokeWidths.append(strokeWidth)
|
||
if len(strokeWidths) > so.stroke_widths_max:
|
||
for strokeWidth in strokeWidths:
|
||
if strokeWidth is None:
|
||
inkex.utils.debug("stroke width: default (None, system standard value)")
|
||
elif strokeWidth == "none":
|
||
inkex.utils.debug("stroke width: none (invisible)")
|
||
else:
|
||
swConverted = self.svg.uutounit(float(self.svg.unittouu(strokeWidth))) #possibly w/o units. we unify to some internal float. The value "none" converts to 0.0
|
||
inkex.utils.debug("stroke width: {}px ({}mm)".format(
|
||
round(self.svg.uutounit(swConverted, "px"),4),
|
||
round(self.svg.uutounit(swConverted, "mm"),4),
|
||
))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} different stroke widths in total".format(len(strokeWidths)))
|
||
if so.show_expert_tips is True and len(strokeWidths) > so.stroke_widths_max:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Colors/Gradients/Filters > Cleanup Styles'\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Styles to Layers'"
|
||
)
|
||
|
||
'''
|
||
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.
|
||
'''
|
||
if so.checks == "check_all" or so.cosmestic_dashes is True:
|
||
inkex.utils.debug("\n---------- Cosmetic dashes")
|
||
strokeDasharrays = []
|
||
for element in shapes:
|
||
strokeDasharray = element.style.get('stroke-dasharray')
|
||
if strokeDasharray is not None and strokeDasharray != 'none' and strokeDasharray not in strokeDasharrays:
|
||
strokeDasharrays.append(strokeDasharray)
|
||
|
||
for strokeDasharray in strokeDasharrays:
|
||
inkex.utils.debug("stroke dash array {}".format(strokeDasharray))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} different stroke dash arrays in total".format(len(strokeDasharrays)))
|
||
if so.show_expert_tips is True and len(strokeDasharrays) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Convert dashes to real paths"
|
||
)
|
||
|
||
'''
|
||
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.
|
||
Please transfer styles from layers/groups level to element level! You can use "Cleanup Styles" extension to do that
|
||
'''
|
||
if so.checks == "check_all" or so.invisible_shapes is True:
|
||
inkex.utils.debug("\n---------- Invisible shapes")
|
||
invisibles = []
|
||
for element in shapes:
|
||
if element.tag not in (inkex.addNS('tspan','svg')) and element.get('inkscape:groupmode') != 'layer' and not isinstance(element, inkex.Group):
|
||
strokeAttr = element.get('stroke') #same information could be in regular attribute instead nested in style attribute
|
||
if strokeAttr is None or strokeAttr == "none":
|
||
strokeVis = 0
|
||
elif strokeAttr in ('#ffffff', 'white', 'rgb(255,255,255)'):
|
||
strokeVis = 0
|
||
else:
|
||
strokeVis = 1
|
||
stroke = element.style.get('stroke')
|
||
if stroke is not None:
|
||
if stroke == "none":
|
||
strokeVis = 0
|
||
elif stroke in ('#ffffff', 'white', 'rgb(255,255,255)'):
|
||
strokeVis = 0
|
||
else:
|
||
strokeVis = 1
|
||
|
||
|
||
strokeWidthAttr = element.get('stroke-width') #same information could be in regular attribute instead nested in style attribute
|
||
if strokeWidthAttr == "none":
|
||
widthVis = 0
|
||
elif strokeWidthAttr is not None and self.svg.unittouu(strokeWidthAttr) < 0.005: #really thin (0,005pc = 0,080px)
|
||
widthVis = 0
|
||
else:
|
||
widthVis = 1
|
||
stroke_width = element.style.get('stroke-width')
|
||
if stroke_width is not None:
|
||
if stroke_width == "none":
|
||
widthVis = 0
|
||
elif stroke_width is not None and self.svg.unittouu(stroke_width) < 0.005: #really thin (0,005pc = 0,080px)
|
||
widthVis = 0
|
||
else:
|
||
widthVis = 1
|
||
|
||
|
||
strokeOpacityAttr = element.get('stroke-opacity') #same information could be in regular attribute instead nested in style attribute
|
||
if strokeOpacityAttr == "none":
|
||
strokeOpacityVis = 0
|
||
elif strokeOpacityAttr is not None and self.svg.unittouu(strokeOpacityAttr) < 0.05: #nearly invisible (<5% opacity)
|
||
strokeOpacityVis = 0
|
||
else:
|
||
strokeOpacityVis = 1
|
||
stroke_opacity = element.style.get('stroke-opacity')
|
||
if stroke_opacity is not None:
|
||
if stroke_opacity == "none": #none means visible!
|
||
strokeOpacityVis = 1
|
||
elif stroke_opacity is not None and self.svg.unittouu(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
|
||
fillAttr = element.get('fill') #same information could be in regular attribute instead nested in style attribute
|
||
if fillAttr is None or fillAttr == "none":
|
||
fillVis = 0
|
||
elif fillAttr in invisColors:
|
||
fillVis = 0
|
||
else:
|
||
fillVis = 1
|
||
fill = element.style.get('fill')
|
||
if fill is not None:
|
||
if fill == "none": #none means invisible! (opposite of stroke behaviour)
|
||
fillVis = 0
|
||
elif fill in invisColors:
|
||
fillVis = 0
|
||
else:
|
||
fillVis = 1
|
||
|
||
|
||
fillOpacityAttr = element.get('fill-opacity') #same information could be in regular attribute instead nested in style attribute
|
||
if fillOpacityAttr == "none":
|
||
fillOpacityVis = 0
|
||
elif strokeOpacityAttr is not None and self.svg.unittouu(fillOpacityAttr) < 0.05: #nearly invisible (<5% opacity)
|
||
fillOpacityVis = 0
|
||
else:
|
||
fillOpacityVis = 1
|
||
fill_opacity = element.style.get('fill-opacity')
|
||
if fill_opacity is not None:
|
||
if fill_opacity == "none":
|
||
fillOpacityVis = 0
|
||
elif fill_opacity is not None and self.svg.unittouu(fill_opacity) < 0.05: #nearly invisible (<5% opacity)
|
||
fillOpacityVis = 0
|
||
else:
|
||
fillOpacityVis = 1
|
||
|
||
|
||
display = element.style.get('display')
|
||
if display == "none":
|
||
displayVis = 0
|
||
else:
|
||
displayVis = 1
|
||
displayAttr = element.get('display') #same information could be in regular attribute instead nested in style attribute
|
||
if displayAttr == "none":
|
||
displayAttrVis = 0
|
||
else:
|
||
displayAttrVis = 1
|
||
|
||
|
||
#check for svg:path elements which have consistent slope (straight lines) and no a defined fill and no stroke. such (poly)lines are still not visible
|
||
pathVis = 1
|
||
if element.tag == inkex.addNS('path','svg') and fillVis == 1 and strokeVis == 0:
|
||
segments = element.path.to_arrays()
|
||
chars = set('aAcCqQtTsS')
|
||
if not any((c in chars) for c in str(element.path)): #skip beziers (we only check for polylines)
|
||
slopes = []
|
||
for i in range(0, len(segments)):
|
||
if i > 0:
|
||
if segments[i][0].lower() == 'z' or segments[i-1][0].lower() == 'z':
|
||
continue #skip closed contours in combined path
|
||
x1, y1, x2, y2 = segments[i-1][1][0], segments[i-1][1][1], segments[i][1][0], segments[i][1][1]
|
||
if x1 < x2:
|
||
p0 = [x1, y1]
|
||
p1 = [x2, y2]
|
||
else:
|
||
p0 = [x2, y2]
|
||
p1 = [x1, y1]
|
||
dx = p1[0] - p0[0]
|
||
if dx == 0:
|
||
slope = sys.float_info.max #vertical
|
||
else:
|
||
slope = (p1[1] - p0[1]) / dx
|
||
slope = round(slope, 6)
|
||
if slope not in slopes:
|
||
slopes.append(slope)
|
||
if len(slopes) < 2:
|
||
pathVis = 0
|
||
|
||
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
|
||
if (strokeVis == 0 or widthVis == 0 or strokeOpacityVis == 0):
|
||
strokeInvis = True
|
||
else:
|
||
strokeInvis = False
|
||
if (fillVis == 0 or fillOpacityVis == 0):
|
||
fillInvis = True
|
||
else:
|
||
fillInvis = False
|
||
flags = "id={},strokeVis={},widthVis={},strokeOpacityVis={}=>strokeInvisble:{}|fillVis={},fillOpacityVis={}=>fillInvisble:{}|displayVis={},displayAttrVis=, {}|pathVis={}"\
|
||
.format(element.get('id'), strokeVis, widthVis, strokeOpacityVis, strokeInvis, fillVis, fillOpacityVis, fillInvis, displayVis, displayAttrVis, pathVis)
|
||
if strokeInvis is True and fillInvis is True:
|
||
if element not in invisibles:
|
||
invisibles.append(flags)
|
||
if displayVis == 0 or displayAttrVis == 0:
|
||
if element not in invisibles:
|
||
invisibles.append(flags)
|
||
if pathVis == 0:
|
||
if element not in invisibles:
|
||
invisibles.append(flags)
|
||
for invisible in invisibles:
|
||
inkex.utils.debug(invisible)
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} invisible shapes in total".format(len(invisibles)))
|
||
if so.show_expert_tips is True and len(invisibles) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Colors/Gradients/Filters > Cleanup Styles'\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Styles to Layers'"
|
||
)
|
||
|
||
'''
|
||
Additionally, opacities less than 1.0 cause problems in most laser softwares. Please
|
||
adjust all strokes to use full opacity.
|
||
'''
|
||
if so.checks == "check_all" or so.opacities is True:
|
||
inkex.utils.debug("\n---------- Objects with transparencies < 1.0")
|
||
transparencies = []
|
||
for element in shapes:
|
||
strokeOpacityAttr = element.get('stroke-opacity') #same information could be in regular attribute instead nested in style attribute
|
||
if strokeOpacityAttr is not None and strokeOpacityAttr not in transparencies:
|
||
if float(strokeOpacityAttr) < 1.0:
|
||
transparencies.append([element, strokeOpacityAttr, "stroke-opacity"])
|
||
stroke_opacity = element.style.get('stroke-opacity')
|
||
if stroke_opacity is not None and stroke_opacity not in transparencies:
|
||
if stroke_opacity != "none":
|
||
if float(stroke_opacity) < 1.0:
|
||
transparencies.append([element, stroke_opacity, "stroke-opacity"])
|
||
|
||
fillOpacityAttr = element.get('fill-opacity') #same information could be in regular attribute instead nested in style attribute
|
||
if fillOpacityAttr is not None and fillOpacityAttr not in transparencies:
|
||
if float(fillOpacityAttr) < 1.0:
|
||
transparencies.append([element, fillOpacityAttr, "fill-opacity"])
|
||
fill_opacity = element.style.get('fill-opacity')
|
||
if fill_opacity is not None and fill_opacity not in transparencies:
|
||
if fill_opacity != "none":
|
||
if float(fill_opacity) < 1.0:
|
||
transparencies.append([element, fill_opacity, "fill-opacity"])
|
||
|
||
opacityAttr = element.get('opacity') #same information could be in regular attribute instead nested in style attribute
|
||
if opacityAttr is not None and opacityAttr not in transparencies:
|
||
if float(opacityAttr) < 1.0:
|
||
transparencies.append([element, opacityAttr, "opacity"])
|
||
opacity = element.style.get('opacity')
|
||
if opacity is not None and opacity not in transparencies:
|
||
if opacity != "none":
|
||
if float(opacity) < 1.0:
|
||
transparencies.append([element, opacity, "opacity"])
|
||
|
||
|
||
for transparency in transparencies:
|
||
inkex.utils.debug("id={}, transparency={}, attribute={}".format(transparency[0].get('id'), transparency[1], transparency[2]))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} objects with transparencies < 1.0 in total".format(len(transparencies)))
|
||
if so.show_expert_tips is True and len(transparencies) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - should be set to 1.0\n"\
|
||
" - Use 'FabLab Chemnitz > Colors/Gradients/Filters > Cleanup Styles'\n"\
|
||
" - Use 'FabLab Chemnitz > Groups and Layers > Styles to Layers'"
|
||
)
|
||
|
||
|
||
'''
|
||
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
|
||
'''
|
||
if so.checks == "check_all" or so.pointy_paths is True:
|
||
inkex.utils.debug("\n---------- Pointy paths")
|
||
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)
|
||
for pointyPath in pointyPaths:
|
||
inkex.utils.debug("id={}".format(pointyPath.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} pointy paths in total".format(len(pointyPaths)))
|
||
if so.show_expert_tips is True and len(pointyPaths) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - should be deleted as they do not contain any valid path data.\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Pointy Paths'"\
|
||
)
|
||
|
||
'''
|
||
Combined paths make trouble with vector sorting algorithm. Check which paths could be broken apart
|
||
'''
|
||
if so.checks == "check_all" or so.combined_paths is True:
|
||
inkex.utils.debug("\n---------- Combined paths")
|
||
combinedPaths = []
|
||
for element in shapes:
|
||
if isinstance(element, inkex.PathElement):
|
||
break_paths = element.path.break_apart()
|
||
if len(break_paths) > 2:
|
||
combinedPaths.append([element, len(break_paths)])
|
||
for combinedPath in combinedPaths:
|
||
inkex.utils.debug("id={} has sub paths: {}".format(combinedPath[0].get('id'), combinedPath[1]))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} combined paths in total".format(len(combinedPaths)))
|
||
if so.show_expert_tips is True and len(combinedPaths) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - break apart pressing CTRL + SHIFT + K"
|
||
)
|
||
|
||
'''
|
||
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.
|
||
'''
|
||
if so.checks == "check_all" or so.transformations is True:
|
||
inkex.utils.debug("\n---------- Transformations")
|
||
transformations = []
|
||
for element in shapes:
|
||
if element.get('transform') is not None:
|
||
transformations.append(element)
|
||
|
||
for transformation in transformations:
|
||
inkex.utils.debug("transformation in id={}".format(transformation.get('id')))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} transformation in total".format(len(transformations)))
|
||
if so.show_expert_tips is True and len(transformations) > 0:
|
||
inkex.utils.debug("TIPS:\n"\
|
||
" - Use 'FabLab Chemnitz > Transformations > Apply Transformations' to remove all transformations, making objects absolute")
|
||
|
||
|
||
'''
|
||
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.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"):
|
||
shortPaths.append([element, stotal])
|
||
totalDropLength += stotal
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} short paths in total".format(len(shortPaths)))
|
||
if totalDropLength > 0:
|
||
inkex.utils.debug("{:0.2f}% of total ({:0.2f} mm /{:0.2f} mm)".format(
|
||
100 * totalDropLength / totalLength,
|
||
self.svg.uutounit(str(totalDropLength), "mm"),
|
||
self.svg.uutounit(str(totalLength), "mm")
|
||
))
|
||
for shortPath in shortPaths:
|
||
inkex.utils.debug("id={}, length={}mm".format(shortPath[0].get('id'), round(self.svg.uutounit(str(shortPath[1]), "mm"), 3)))
|
||
|
||
'''
|
||
Really short paths can cause issues with laser cutter mechanics and should be avoided to
|
||
have healthier stepper motor belts, etc.
|
||
|
||
Peck Sidara from Epilog:
|
||
"Most of the acceleration of speed occurs from the 1-20% range, there is a difference between say 30% and 90%speed
|
||
but due to the many variables (length of nodes, shape and contour of nodes), you may not see a noticable difference in time.
|
||
Additional variables include acceleration, deceleration and how our laser handles/translates the vector data."
|
||
'''
|
||
if so.checks == "check_all" or so.cutting_estimation is True:
|
||
inkex.utils.debug("\n---------- Cutting time estimation (Epilog Lasers)")
|
||
totalCuttingLength = 0
|
||
totalTravelLength = 0
|
||
cuttingPathCount = 0
|
||
travelPathCount = 0
|
||
|
||
for element in shapes:
|
||
if isinstance(element, inkex.PathElement):
|
||
slengths, stotal = csplength(element.path.transform(element.composed_transform()).to_superpath())
|
||
if "-travelLine" in element.get('id'): #we use that id scheme together with the extension "Draw Directions / Travel Moves"
|
||
totalTravelLength += stotal
|
||
travelPathCount += 1
|
||
elif "markerId-" in element.get('id'):
|
||
pass #we skip the path "markerId-<nr>", possibly generated by the extension "Draw Directions / Travel Moves
|
||
else:
|
||
totalCuttingLength += stotal
|
||
cuttingPathCount += 1
|
||
totalLength = totalCuttingLength + totalTravelLength
|
||
v_travel = so.max_travel_speed #this is always at maximum
|
||
inkex.utils.debug("total cutting paths={}".format(cuttingPathCount))
|
||
inkex.utils.debug("total travel paths={}".format(travelPathCount))
|
||
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("(measured) travel length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalTravelLength), "mm"), self.svg.uutounit(str(totalTravelLength), "mm")))
|
||
inkex.utils.debug("(measured) total length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalLength), "mm"), self.svg.uutounit(str(totalLength), "mm")))
|
||
inkex.utils.debug("travel speed={:06.2f}mm/s".format(v_travel))
|
||
''' 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."
|
||
'''
|
||
speedFactors = []
|
||
try:
|
||
for speed in re.findall(r"[+]?\d*\.\d+|\d+", self.options.cutting_speedfactors): #allow only positive values
|
||
if float(speed) > 0:
|
||
speedFactors.append(float(speed))
|
||
speedFactors = sorted(speedFactors)[::-1]
|
||
except:
|
||
inkex.utils.debug("Error parsing cutting estimation speeds. Please try again!")
|
||
exit(1)
|
||
for speedFactor in speedFactors:
|
||
speedFactorR = speedFactor / 100.0
|
||
adjusted_speed = 480.0 / so.max_cutting_speed #empiric - found out by trying for hours ...
|
||
empiric_scale = 1 + (speedFactorR**2) / 15.25 #empiric - found out by trying for hours ...
|
||
v_cut = so.max_cutting_speed * speedFactorR
|
||
tsec_cut = (self.svg.uutounit(str(totalCuttingLength)) / (adjusted_speed * so.max_cutting_speed * speedFactorR)) * empiric_scale
|
||
tsec_travel = self.svg.uutounit(str(totalTravelLength)) / v_travel
|
||
tsec_total = so.job_time_offset + tsec_cut + tsec_travel
|
||
minutes, seconds = divmod(tsec_total, 60) # split the seconds to minutes and seconds
|
||
seconds_for_price = seconds
|
||
#round seconds up to 30 or 60
|
||
if so.round_times is True:
|
||
if seconds_for_price < 30:
|
||
seconds_for_price = 30
|
||
if seconds_for_price > 30 and seconds_for_price != 60:
|
||
seconds_for_price = 60
|
||
|
||
partial_minutes = round(seconds_for_price/60 * 2) / 2
|
||
costs = so.price_per_minute_gross * (minutes + partial_minutes)
|
||
if "{:02.0f}".format(seconds) == "60": #for formatting reasons
|
||
seconds = 0
|
||
minutes += 1
|
||
inkex.utils.debug("@{:05.1f}% (cut={:06.2f}mm/s > {:03.0f}min {:02.0f}sec | cost={:02.0f}€".format(speedFactor, v_cut, minutes, seconds, costs))
|
||
|
||
|
||
''' 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 = 0mm):
|
||
@ 100% = 13:45 = 825s -> 121,21mm/s
|
||
@ 090% = 15:12 = 912s -> 109,65mm/s
|
||
@ 080% = 17:01 = 1021s -> 97,94mm/s
|
||
@ 070% = 19:21 = 1161s -> 86,13mm/s
|
||
@ 060% = 22:28 = 1348s -> 74,18mm/s
|
||
@ 050% = 26:49 = 1609s -> 62,15mm/s
|
||
@ 040% = 33:21 = 2001s -> 49,98mm/s
|
||
@ 030% = 44:13 = 2653s -> 37,69mm/s
|
||
@ 020% = 65:51 = 3951s -> 25,31mm/s
|
||
@ 010% = 130:52 = 7852s -> 12,74mm/s
|
||
@ 009% = 145:21 = 8721s -> 11,47mm/s
|
||
@ 008% = 163:27 = 9807s -> 10,20mm/s
|
||
@ 007% = 186:44 = 11204s -> 8,93mm/s
|
||
@ 006% = 217:48 = 13068s -> 7,65mm/s
|
||
@ 005% = 261:18 = 15678s -> 6,38mm/s
|
||
@ 004% = 326:34 = 19594s -> 5,10mm/s
|
||
@ 003% = 435:21 = 26121s -> 3,83mm/s
|
||
@ 002% = 652:57 = 39177s -> 2,55mm/s
|
||
@ 001% = 1305:49 = 78349s -> 1,28mm/s
|
||
|
||
It does not matter how slow we configure the laser, the job time estimation always has the same amount of travel time
|
||
(if we have some travel moves to perform), so the travel speed is always constant. The max. travel speed of Fusion Pro 32
|
||
is between 425mm/s and 460mm/s (measured by Mario by hand at different laser jobs).
|
||
If the laser is in X=0 Y=0 the jobs needs ~2 seconds to start moving and firing the laser. We use this as constant offset
|
||
'''
|
||
|
||
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)".format(so.nodes_per_path_max, round(so.nodes_per_path_interval, 3)))
|
||
heavyPaths = []
|
||
totalNodesCount = 0
|
||
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 stotal > 0: #ignore pointy paths, which might generate zero length paths. Use the pointy path check to find them!
|
||
if nodes / stotal > so.nodes_per_path_max / self.svg.unittouu(str(so.nodes_per_path_interval) + "mm"):
|
||
heavyPaths.append([element, nodes, stotal])
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} Heavy node-loaded paths in total".format(len(heavyPaths)))
|
||
if so.show_expert_tips is True and len(heavyPaths) > 0:
|
||
inkex.utils.debug("Paths with a high amount of nodes will cause issues because each node means slowing down the laser mechanics. Otherwise we will get stuttering movements.")
|
||
inkex.utils.debug("SOME TIPS TO REDUCE NODES:\n"\
|
||
" - Reduce count of nodes on paths / reduce duplicate segments / remove useless:\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Duplicate Path Nodes'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Purge Duplicate Path Segments'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Intersect/Cut/Purge > Remove Duplicate Line Segments'\n"\
|
||
" - Use 'FabLab Chemnitz > Paths Join/Order > Chain Paths'\n"\
|
||
" - Simplify by CTRL+L\n"
|
||
)
|
||
for heavyPath in heavyPaths:
|
||
totalNodesCount += heavyPath[1]
|
||
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)
|
||
)
|
||
)
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} total nodes on paths".format(totalNodesCount))
|
||
pathCount = 0
|
||
for key in counter.keys():
|
||
if key == "path":
|
||
pathCount = counter[key]
|
||
if pathCount > 0:
|
||
inkex.utils.debug("Average nodes per path: {:0.0f}".format(totalNodesCount/pathCount))
|
||
|
||
|
||
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(element.composed_transform())
|
||
if ebbox is not None: #pointy paths for example could generate non-bbox shapes. So we ignore them here
|
||
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)
|
||
vxMin, vyMin, vxMax, vyMax = self.svg.get_viewbox()
|
||
pagew = round(vxMax - vxMin, precision)
|
||
pageh = round(vyMax - vyMin, 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"])
|
||
for elementOutside in elementsOutside:
|
||
inkex.utils.debug("id={}, status={}".format(
|
||
elementOutside[0].get('id'),
|
||
elementOutside[1]
|
||
)
|
||
)
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} Elements outside canvas or touching the border in total".format(len(elementsOutside)))
|
||
if so.show_expert_tips is True and len(elementsOutside) > 0:
|
||
inkex.utils.debug("SOME TIPS:\n"\
|
||
" - Elements outside canvas or touching the border. These are critical because they won't be lasered or not correctly lasered"
|
||
)
|
||
|
||
|
||
if so.checks == "check_all" or so.non_path_shapes is True:
|
||
inkex.utils.debug("\n---------- Non-path shapes")
|
||
nonPathShapes = []
|
||
for element in shapes:
|
||
if not isinstance(element, inkex.PathElement) and not isinstance(element, inkex.Group):
|
||
nonPathShapes.append(element)
|
||
for nonPathShape in nonPathShapes:
|
||
inkex.utils.debug("id={}, type={}".format(nonPathShape.get('id'), nonPathShape.tag.replace("{http://www.w3.org/2000/svg}", "")))
|
||
if so.show_issues_only is False:
|
||
inkex.utils.debug("{} non-path shapes in total".format(len(nonPathShapes)))
|
||
if so.show_expert_tips is True and len(nonPathShapes) > 0:
|
||
inkex.utils.debug("SOME TIPS:\n"\
|
||
" - Shapes like rectangles, ellipses, arcs, spirals should be converted to svg:path"
|
||
)
|
||
|
||
exit(0)
|
||
|
||
if __name__ == '__main__':
|
||
LaserCheck().run() |