From 1d46b43c4f9ae218b33d1ec740fba8220d3e1d03 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 21 Sep 2020 03:27:52 +0300 Subject: [PATCH] - in SVG parser: made sure that the minimum number of steps to approximate an arc/circle/bezier is 10 --- CHANGELOG.md | 1 + appParsers/ParseGerber.py | 20 ++++++----- appParsers/ParseSVG.py | 71 +++++++++++++++++++++++++-------------- appTools/ToolQRCode.py | 4 ++- app_Main.py | 7 ++-- camlib.py | 6 ++-- 6 files changed, 67 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a5681e..c95cfe46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG for FlatCAM beta 20.09.2020 - in CNCJob UI Autolevelling: on manual add of probe points, only voronoi diagram is calculated +- in SVG parser: made sure that the minimum number of steps to approximate an arc/circle/bezier is 10 19.09.2020 diff --git a/appParsers/ParseGerber.py b/appParsers/ParseGerber.py index ec967140..d4e336c7 100644 --- a/appParsers/ParseGerber.py +++ b/appParsers/ParseGerber.py @@ -1786,16 +1786,16 @@ class Gerber(Geometry): self.scale(factor, factor) return factor - def import_svg(self, filename, object_type='gerber', flip=True, units='MM'): + def import_svg(self, filename, object_type='gerber', flip=True, units=None): """ Imports shapes from an SVG file into the object's geometry. - :param filename: Path to the SVG file. - :type filename: str - :param object_type: parameter passed further along - :param flip: Flip the vertically. - :type flip: bool - :param units: FlatCAM units + :param filename: Path to the SVG file. + :type filename: str + :param object_type: parameter passed further along + :param flip: Flip the vertically. + :type flip: bool + :param units: FlatCAM units :return: None """ @@ -1810,7 +1810,9 @@ class Gerber(Geometry): # w = float(svg_root.get('width')) h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet - geos = getsvggeo(svg_root, 'gerber') + units = self.app.defaults['units'] if units is None else units + res = self.app.defaults['gerber_circle_steps'] + geos = getsvggeo(svg_root, 'gerber', units=units, res=res) if flip: geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] @@ -1837,7 +1839,7 @@ class Gerber(Geometry): geo_qrcode = [] geo_qrcode.append(Polygon(geos[0].exterior)) for i_el in geos[0].interiors: - geo_qrcode.append(Polygon(i_el).buffer(0)) + geo_qrcode.append(Polygon(i_el).buffer(0, resolution=res)) for poly in geo_qrcode: geos.append(poly) diff --git a/appParsers/ParseSVG.py b/appParsers/ParseSVG.py index 1321b973..fcdd2b17 100644 --- a/appParsers/ParseSVG.py +++ b/appParsers/ParseSVG.py @@ -38,9 +38,9 @@ def svgparselength(lengthstr): Parse an SVG length string into a float and a units string, if any. - :param lengthstr: SVG length string. - :return: Number and units pair. - :rtype: tuple(float, str|None) + :param lengthstr: SVG length string. + :return: Number and units pair. + :rtype: tuple(float, str|None) """ integer_re_str = r'[+-]?[0-9]+' @@ -58,7 +58,7 @@ def svgparselength(lengthstr): return -def path2shapely(path, object_type, res=1.0): +def path2shapely(path, object_type, res=1.0, units='MM'): """ Converts an svg.path.Path into a Shapely Polygon or LinearString. @@ -66,6 +66,8 @@ def path2shapely(path, object_type, res=1.0): :param path: svg.path.Path instance :param object_type: :param res: Resolution (minimum step along path) + :param units: FlatCAM units + :type units: str :return: Shapely geometry object :rtype : Polygon :rtype : LineString @@ -98,10 +100,14 @@ def path2shapely(path, object_type, res=1.0): # steps = int(length / res + 0.5) steps = int(length) * 2 + if units == 'IN': + steps *= 25 + # solve error when step is below 1, - # it may cause other problems, but LineString needs at least two points - if steps == 0: - steps = 1 + # it may cause other problems, but LineString needs at least two points + # later edit: made the minimum nr of steps to be 10; left it like that to see that steps can be 0 + if steps == 0 or steps < 10: + steps = 10 frac = 1.0 / steps @@ -183,7 +189,7 @@ def svgrect2shapely(rect, n_points=32): :param rect: Rect Element :type rect: xml.etree.ElementTree.Element - :param n_points: number of points to approximate circles + :param n_points: number of points to approximate rectangles corners when having rounded corners :type n_points: int :return: shapely.geometry.polygon.LinearRing """ @@ -247,14 +253,16 @@ def svgrect2shapely(rect, n_points=32): # return LinearRing(pts) -def svgcircle2shapely(circle): +def svgcircle2shapely(circle, n_points=64): """ Converts an SVG circle into Shapely geometry. - :param circle: Circle Element - :type circle: xml.etree.ElementTree.Element - :return: Shapely representation of the circle. - :rtype: shapely.geometry.polygon.LinearRing + :param circle: Circle Element + :type circle: xml.etree.ElementTree.Element + :param n_points: circle resolution; nr of points to b e used to approximate a circle + :type n_points: int + :return: Shapely representation of the circle. + :rtype: shapely.geometry.polygon.LinearRing """ # cx = float(circle.get('cx')) # cy = float(circle.get('cy')) @@ -263,8 +271,7 @@ def svgcircle2shapely(circle): cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet r = svgparselength(circle.get('r'))[0] # TODO: No units support yet - # TODO: No resolution specified. - return Point(cx, cy).buffer(r) + return Point(cx, cy).buffer(r, resolution=n_points) def svgellipse2shapely(ellipse, n_points=64): @@ -318,22 +325,35 @@ def svgpolyline2shapely(polyline): return LineString(points) -def svgpolygon2shapely(polygon): +def svgpolygon2shapely(polygon, n_points=64): + """ + Convert a SVG polygon to a Shapely Polygon. + + :param polygon: + :type polygon: + :param n_points: circle resolution; nr of points to b e used to approximate a circle + :type n_points: int + :return: Shapely Polygon + """ ptliststr = polygon.get('points') points = parse_svg_point_list(ptliststr) - return Polygon(points).buffer(0) + return Polygon(points).buffer(0, resolution=n_points) # return LinearRing(points) -def getsvggeo(node, object_type, root=None): +def getsvggeo(node, object_type, root=None, units='MM', res=64): """ Extracts and flattens all geometry from an SVG node into a list of Shapely geometry. :param node: xml.etree.ElementTree.Element :param object_type: + :param root: + :param units: FlatCAM units + :param res: resolution to be used for circles bufferring + :return: List of Shapely geometry :rtype: list """ @@ -346,7 +366,7 @@ def getsvggeo(node, object_type, root=None): # Recurse if len(node) > 0: for child in node: - subgeo = getsvggeo(child, object_type, root) + subgeo = getsvggeo(child, object_type, root=root, units=units, res=res) if subgeo is not None: geo += subgeo @@ -354,28 +374,28 @@ def getsvggeo(node, object_type, root=None): elif kind == 'path': log.debug("***PATH***") P = parse_path(node.get('d')) - P = path2shapely(P, object_type) + P = path2shapely(P, object_type, units=units) # for path, the resulting geometry is already a list so no need to create a new one geo = P elif kind == 'rect': log.debug("***RECT***") - R = svgrect2shapely(node) + R = svgrect2shapely(node, n_points=res) geo = [R] elif kind == 'circle': log.debug("***CIRCLE***") - C = svgcircle2shapely(node) + C = svgcircle2shapely(node, n_points=res) geo = [C] elif kind == 'ellipse': log.debug("***ELLIPSE***") - E = svgellipse2shapely(node) + E = svgellipse2shapely(node, n_points=res) geo = [E] elif kind == 'polygon': log.debug("***POLYGON***") - poly = svgpolygon2shapely(node) + poly = svgpolygon2shapely(node, n_points=res) geo = [poly] elif kind == 'line': @@ -395,7 +415,7 @@ def getsvggeo(node, object_type, root=None): href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href'] ref = root.find(".//*[@id='%s']" % href.replace('#', '')) if ref is not None: - geo = getsvggeo(ref, object_type, root) + geo = getsvggeo(ref, object_type, root=root, units=units, res=res) else: log.warning("Unknown kind: " + kind) @@ -437,6 +457,7 @@ def getsvgtext(node, object_type, units='MM'): :param node: xml.etree.ElementTree.Element :param object_type: + :param units: FlatCAM units :return: List of Shapely geometry :rtype: list """ diff --git a/appTools/ToolQRCode.py b/appTools/ToolQRCode.py index ed24d221..924f6687 100644 --- a/appTools/ToolQRCode.py +++ b/appTools/ToolQRCode.py @@ -427,7 +427,9 @@ class QRCode(AppTool): # h = float(svg_root.get('height')) # w = float(svg_root.get('width')) h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet - geos = getsvggeo(svg_root, object_type) + units = self.app.defaults['units'] if units is None else units + res = self.app.defaults['geometry_circle_steps'] + geos = getsvggeo(svg_root, object_type, units=units, res=res) if flip: geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] diff --git a/app_Main.py b/app_Main.py index ccfe283e..47d36696 100644 --- a/app_Main.py +++ b/app_Main.py @@ -8873,12 +8873,9 @@ class App(QtCore.QObject): units = self.defaults['units'].upper() def obj_init(geo_obj, app_obj): - if obj_type == "geometry": - geo_obj.import_svg(filename, obj_type, units=units) - elif obj_type == "gerber": - geo_obj.import_svg(filename, obj_type, units=units) - + geo_obj.import_svg(filename, obj_type, units=units) geo_obj.multigeo = True + with open(filename) as f: file_content = f.read() geo_obj.source_file = file_content diff --git a/camlib.py b/camlib.py index 32785c11..04e0cfb7 100644 --- a/camlib.py +++ b/camlib.py @@ -1090,7 +1090,7 @@ class Geometry(object): else: yield item - def import_svg(self, filename, object_type=None, flip=True, units='MM'): + def import_svg(self, filename, object_type=None, flip=True, units=None): """ Imports shapes from an SVG file into the object's geometry. @@ -1114,7 +1114,9 @@ class Geometry(object): # w = float(svg_root.get('width')) h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet - geos = getsvggeo(svg_root, object_type) + units = self.app.defaults['units'] if units is None else units + res = self.app.defaults['geometry_circle_steps'] + geos = getsvggeo(svg_root, object_type, units=units, res=res) if flip: geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]