From a5ff8c574a07e4662f32c8e345b5e20ce0fb689d Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 11:38:14 +0000 Subject: [PATCH 01/16] Added initial svg export functionality --- FlatCAMApp.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- FlatCAMGUI.py | 4 +++ camlib.py | 17 ++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e5b94893..3069eaf3 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -25,7 +25,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +from xml.dom.minidom import parseString as parse_xml_string ######################################## ## App ## @@ -451,6 +451,7 @@ class App(QtCore.QObject): self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg) + self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg) self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject) self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas) self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True)) @@ -1577,6 +1578,42 @@ class App(QtCore.QObject): # thread safe. The new_project() self.open_project(filename) + def on_file_exportsvg(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportsvg") + App.log.debug("on_file_exportsvg()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("WARNING: No object selected.") + msg = "Please Select a Geometry object to export" + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + name = self.collection.get_active().options["name"] + + try: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG", + directory=self.get_last_folder(), filter="*.svg") + except TypeError: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG") + + filename = str(filename) + + if str(filename) == "": + self.inform.emit("Export SVG cancelled.") + return + else: + self.export_svg(name, filename) + def on_file_importsvg(self): """ Callback for menu item File->Import SVG. @@ -1661,6 +1698,31 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) + + def export_svg(self, obj_name, filename, outname=None): + """ + Exports a Geometry Object to a SVG File + + :param filename: Path to the SVG file to save to. + :param outname: + :return: + """ + self.log.debug("export_svg()") + + try: + obj = self.collection.get_by_name(str(obj_name)) + except: + return "Could not retrieve object: %s" % obj_name + + # TODO needs size of board / dpi information + + with self.proc_container.new("Exporting SVG") as proc: + svg_elem = obj.export_svg() + svg_elem = "" + svg_elem + "" + doc = parse_xml_string(svg_elem) + with open(filename, 'w') as fp: + fp.write(doc.toprettyxml()) + def import_svg(self, filename, outname=None): """ Adds a new Geometry Object to the projects and populates @@ -2109,6 +2171,10 @@ class App(QtCore.QObject): return str(self.collection.get_names()) + def export_svg(name, filename): + + self.export_svg(str(name), str(filename)) + def import_svg(filename, *args): a, kwa = h(*args) types = {'outname': str} @@ -3225,6 +3291,13 @@ class App(QtCore.QObject): "> import_svg " + " filename: Path to the file to import." }, + 'export_svg': { + 'fcn': export_svg, + 'help': "Export a Geometry Object as a SVG File\n" + + "> export_svg \n" + + " name: Name of the geometry object to export.\n" + + " filename: Path to the file to export." + }, 'open_gerber': { 'fcn': open_gerber, 'help': "Opens a Gerber file.\n" diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 8bb2445e..3c01d124 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -48,6 +48,10 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) self.menufile.addAction(self.menufileimportsvg) + # Export SVG ... + self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self) + self.menufile.addAction(self.menufileexportsvg) + # Save Project self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self) self.menufile.addAction(self.menufilesaveproject) diff --git a/camlib.py b/camlib.py index f576ed98..8bbd49ca 100644 --- a/camlib.py +++ b/camlib.py @@ -869,6 +869,14 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] + def export_svg(self): + """ + Exports the Gemoetry Object as a SVG Element + + :return: SVG Element + """ + svg_elem = self.solid_geometry.svg() + return svg_elem class ApertureMacro: """ @@ -3313,6 +3321,15 @@ class CNCjob(Geometry): self.create_geometry() + def export_svg(self): + """ + Exports the CNC Job as a SVG Element + + :return: SVG Element + """ + self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + svg_elem = self.solid_geometry.svg() + return svg_elem # def get_bounds(geometry_set): # xmin = Inf From b272329384f5aa49a87ddbd04792abc9444201cd Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 17:25:46 +0000 Subject: [PATCH 02/16] Initial scaling fixes for svg export --- FlatCAMApp.py | 10 +++++++--- camlib.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 3069eaf3..f6f74d47 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1714,11 +1714,15 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs size of board / dpi information + # TODO needs size of board determining + # TODO needs seperate colours for CNCPath Export with self.proc_container.new("Exporting SVG") as proc: - svg_elem = obj.export_svg() - svg_elem = "" + svg_elem + "" + svg_header = ' Date: Mon, 21 Mar 2016 19:34:33 +0000 Subject: [PATCH 03/16] Fixed the scaling issues with the svg export --- FlatCAMApp.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index f6f74d47..fb600282 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1714,15 +1714,32 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs size of board determining # TODO needs seperate colours for CNCPath Export + # The line thickness is only affected by the scaling factor not the tool size + # Use the tool size to determine the scaling factor for line thickness with self.proc_container.new("Exporting SVG") as proc: + exported_svg = obj.export_svg() + + # Determine bounding area for svg export + svgwidth = obj.solid_geometry.bounds[2] - obj.solid_geometry.bounds[0] + svgheight = obj.solid_geometry.bounds[3] - obj.solid_geometry.bounds[1] + minx = obj.solid_geometry.bounds[0] + miny = obj.solid_geometry.bounds[1] - svgheight + + svgwidth = str(svgwidth) + svgheight = str(svgheight) + minx = str(minx) + miny = str(miny) + uom = obj.units.lower() + svg_header = '' svg_header += '' svg_footer = ' ' - svg_elem = svg_header + obj.export_svg() + svg_footer + svg_elem = svg_header + exported_svg + svg_footer doc = parse_xml_string(svg_elem) with open(filename, 'w') as fp: fp.write(doc.toprettyxml()) From 532a821c765d163185aaed1f16c6e7ec4820df06 Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 21:46:29 +0000 Subject: [PATCH 04/16] Fixed the colors with svg exports from cnc jobs for Visicut --- camlib.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/camlib.py b/camlib.py index c9f2f7ff..bc42b5d3 100644 --- a/camlib.py +++ b/camlib.py @@ -3327,8 +3327,28 @@ class CNCjob(Geometry): :return: SVG Element """ + + # This appears to match up distance wise with inkscape + scale = self.options['tooldia'] / 2 + if scale == 0: + scale = 0.05 + + cuts = [] + travels = [] + for g in self.gcode_parsed: + if g['kind'][0] == 'C': cuts.append(g) + if g['kind'][0] == 'T': travels.append(g) + + # Used to determine board size self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) - svg_elem = self.solid_geometry.svg(scale_factor=0.05) + + # Seperate the travels from the cuts for laser cutting under Visicut + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + + svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + return svg_elem # def get_bounds(geometry_set): From 10e9fa74c3226dc076c8c2de00f79344bf0959bf Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 02:25:07 +0000 Subject: [PATCH 05/16] Added some additional checks for the types when exporting, and additional comments --- FlatCAMApp.py | 23 ++++++++++++++++++----- camlib.py | 9 +++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index fb600282..76c72876 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1598,6 +1598,18 @@ class App(QtCore.QObject): msgbox.exec_() return + # Check for more compatible types and add as required + # Excellon not yet supported, there seems to be a list within the Polygon Geometry that shapely's svg export doesn't like + + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob)): + msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + name = self.collection.get_active().options["name"] try: @@ -1714,10 +1726,6 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs seperate colours for CNCPath Export - # The line thickness is only affected by the scaling factor not the tool size - # Use the tool size to determine the scaling factor for line thickness - with self.proc_container.new("Exporting SVG") as proc: exported_svg = obj.export_svg() @@ -1727,12 +1735,15 @@ class App(QtCore.QObject): minx = obj.solid_geometry.bounds[0] miny = obj.solid_geometry.bounds[1] - svgheight + # Convert everything to strings for use in the xml doc svgwidth = str(svgwidth) svgheight = str(svgheight) minx = str(minx) miny = str(miny) uom = obj.units.lower() - + + # Add a SVG Header and footer to the svg output from shapely + # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape svg_header = ' Date: Tue, 22 Mar 2016 09:54:57 +0000 Subject: [PATCH 06/16] This adds a bunch of fixes when exporting svg's from geom's or cncjobs generated from drill files, also adds support for exporting drill files directly as svg's, and should capture any objects that use list within the solid_geometry attribute --- FlatCAMApp.py | 20 +++++++++++++------- camlib.py | 22 +++++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 76c72876..e0dac238 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1599,9 +1599,8 @@ class App(QtCore.QObject): return # Check for more compatible types and add as required - # Excellon not yet supported, there seems to be a list within the Polygon Geometry that shapely's svg export doesn't like - - if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob)): + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob) + and not isinstance(obj, FlatCAMExcellon)): msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." msgbox = QtGui.QMessageBox() msgbox.setInformativeText(msg) @@ -1729,11 +1728,18 @@ class App(QtCore.QObject): with self.proc_container.new("Exporting SVG") as proc: exported_svg = obj.export_svg() + # Sometimes obj.solid_geometry can be a list instead of a Shapely class + # Make sure we see it as a Shapely Geometry class + geom = obj.solid_geometry + if type(obj.solid_geometry) is list: + geom = [cascaded_union(obj.solid_geometry)][0] + + # Determine bounding area for svg export - svgwidth = obj.solid_geometry.bounds[2] - obj.solid_geometry.bounds[0] - svgheight = obj.solid_geometry.bounds[3] - obj.solid_geometry.bounds[1] - minx = obj.solid_geometry.bounds[0] - miny = obj.solid_geometry.bounds[1] - svgheight + svgwidth = geom.bounds[2] - geom.bounds[0] + svgheight = geom.bounds[3] - geom.bounds[1] + minx = geom.bounds[0] + miny = geom.bounds[1] - svgheight # Convert everything to strings for use in the xml doc svgwidth = str(svgwidth) diff --git a/camlib.py b/camlib.py index e59b18c0..9cd11e96 100644 --- a/camlib.py +++ b/camlib.py @@ -875,7 +875,14 @@ class Geometry(object): :return: SVG Element """ - svg_elem = self.solid_geometry.svg(scale_factor=0.05) + # Sometimes self.solid_geometry can be a list instead of a Shapely class + # Make sure we see it as a Shapely Geometry class + geom = self.solid_geometry + if type(self.solid_geometry) is list: + geom = [cascaded_union(self.solid_geometry)][0] + + # Convert to a SVG + svg_elem = geom.svg(scale_factor=0.05) return svg_elem class ApertureMacro: @@ -3345,14 +3352,19 @@ class CNCjob(Geometry): self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) # Convert the cuts and travels into single geometry objects we can render as svg xml - travelsgeom = cascaded_union([geo['geom'] for geo in travels]) - cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + if travels: + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + if cuts: + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) # Render the SVG Xml # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set # It's better to have the travels sitting underneath the cuts for visicut - svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") - svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + svg_elem = "" + if travels: + svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + if cuts: + svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") return svg_elem From ee43d8b920b1d0db22e80e43a5822762a9299000 Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 18:56:04 +0000 Subject: [PATCH 07/16] Additional fixes for export size and flattening the geometry list --- FlatCAMApp.py | 19 +++++++------------ camlib.py | 4 +--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e0dac238..2f540588 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1730,22 +1730,17 @@ class App(QtCore.QObject): # Sometimes obj.solid_geometry can be a list instead of a Shapely class # Make sure we see it as a Shapely Geometry class - geom = obj.solid_geometry - if type(obj.solid_geometry) is list: - geom = [cascaded_union(obj.solid_geometry)][0] - + geom = cascaded_union(obj.flatten()) # Determine bounding area for svg export - svgwidth = geom.bounds[2] - geom.bounds[0] - svgheight = geom.bounds[3] - geom.bounds[1] - minx = geom.bounds[0] - miny = geom.bounds[1] - svgheight + bounds = obj.bounds() + size = obj.size() # Convert everything to strings for use in the xml doc - svgwidth = str(svgwidth) - svgheight = str(svgheight) - minx = str(minx) - miny = str(miny) + svgwidth = str(size[0]) + svgheight = str(size[1]) + minx = str(bounds[0]) + miny = str(bounds[1] - size[1]) uom = obj.units.lower() # Add a SVG Header and footer to the svg output from shapely diff --git a/camlib.py b/camlib.py index 9cd11e96..2ae8471a 100644 --- a/camlib.py +++ b/camlib.py @@ -877,9 +877,7 @@ class Geometry(object): """ # Sometimes self.solid_geometry can be a list instead of a Shapely class # Make sure we see it as a Shapely Geometry class - geom = self.solid_geometry - if type(self.solid_geometry) is list: - geom = [cascaded_union(self.solid_geometry)][0] + geom = cascaded_union(self.flatten()) # Convert to a SVG svg_elem = geom.svg(scale_factor=0.05) From 039a2dd4dc6f7fa7b26346d34887029883998396 Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 23:22:02 +0000 Subject: [PATCH 08/16] Made scale_factor optional for cli, added more comments, removed redundant code --- FlatCAMApp.py | 24 ++++++++++++++---------- camlib.py | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2f540588..6593d9a2 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1710,7 +1710,7 @@ class App(QtCore.QObject): self.inform.emit("Project copy saved to: " + self.project_filename) - def export_svg(self, obj_name, filename, outname=None): + def export_svg(self, obj_name, filename, scale_factor=0.00): """ Exports a Geometry Object to a SVG File @@ -1726,11 +1726,7 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % obj_name with self.proc_container.new("Exporting SVG") as proc: - exported_svg = obj.export_svg() - - # Sometimes obj.solid_geometry can be a list instead of a Shapely class - # Make sure we see it as a Shapely Geometry class - geom = cascaded_union(obj.flatten()) + exported_svg = obj.export_svg(scale_factor=scale_factor) # Determine bounding area for svg export bounds = obj.bounds() @@ -2206,9 +2202,16 @@ class App(QtCore.QObject): return str(self.collection.get_names()) - def export_svg(name, filename): + def export_svg(name, filename, *args): + a, kwa = h(*args) + types = {'scale_factor': float} - self.export_svg(str(name), str(filename)) + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + self.export_svg(str(name), str(filename), **kwa) def import_svg(filename, *args): a, kwa = h(*args) @@ -3329,9 +3332,10 @@ class App(QtCore.QObject): 'export_svg': { 'fcn': export_svg, 'help': "Export a Geometry Object as a SVG File\n" + - "> export_svg \n" + + "> export_svg [-scale_factor <0.0 (float)>]\n" + " name: Name of the geometry object to export.\n" + - " filename: Path to the file to export." + " filename: Path to the file to export.\n" + + " scale_factor: Multiplication factor used for scaling line widths during export." }, 'open_gerber': { 'fcn': open_gerber, diff --git a/camlib.py b/camlib.py index 2ae8471a..23bf1bcf 100644 --- a/camlib.py +++ b/camlib.py @@ -869,18 +869,25 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] - def export_svg(self): + def export_svg(self, scale_factor=0.00): """ Exports the Gemoetry Object as a SVG Element :return: SVG Element """ - # Sometimes self.solid_geometry can be a list instead of a Shapely class - # Make sure we see it as a Shapely Geometry class + # Make sure we see a Shapely Geometry class and not a list geom = cascaded_union(self.flatten()) + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + + # If 0 or less which is invalid then default to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor <= 0: + scale_factor = 0.05 + # Convert to a SVG - svg_elem = geom.svg(scale_factor=0.05) + svg_elem = geom.svg(scale_factor=scale_factor) return svg_elem class ApertureMacro: @@ -3326,17 +3333,26 @@ class CNCjob(Geometry): self.create_geometry() - def export_svg(self): + def export_svg(self, scale_factor=0.00): """ Exports the CNC Job as a SVG Element - :return: SVG Element + :scale_factor: float + :return: SVG Element string """ + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + # If not specified then try and use the tool diameter + # This way what is on screen will match what is outputed for the svg + # This is quite a useful feature for svg's used with visicut - # This appears to match up distance wise with inkscape - scale = self.options['tooldia'] / 2 - if scale == 0: - scale = 0.05 + if scale_factor <= 0: + scale_factor = self.options['tooldia'] / 2 + + # If still 0 then defailt to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor == 0: + scale_factor = 0.05 # Seperate the list of cuts and travels into 2 distinct lists # This way we can add different formatting / colors to both @@ -3360,9 +3376,9 @@ class CNCjob(Geometry): # It's better to have the travels sitting underneath the cuts for visicut svg_elem = "" if travels: - svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D") if cuts: - svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF") return svg_elem From 790f53dd55e26e80ee56541073a892d4fe4fe391 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 23 Mar 2016 11:06:48 -0400 Subject: [PATCH 09/16] Blocking in shell functions. Test for exception handling. See #196. --- FlatCAMApp.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e5b94893..6626cac9 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2107,7 +2107,27 @@ class App(QtCore.QObject): except Exception as e: return str(e) - return str(self.collection.get_names()) + def mytest2(*args): + to = int(args[0]) + + try: + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break + + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") + + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() + + return str(self.collection.get_names()) + + except Exception as e: + return str(e) def import_svg(filename, *args): a, kwa = h(*args) @@ -3215,6 +3235,10 @@ class App(QtCore.QObject): 'fcn': mytest, 'help': "Test function. Only for testing." }, + 'mytest2': { + 'fcn': mytest2, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." From 95676f21e2d973c0486e55f87beaede434f15764 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 23 Mar 2016 14:58:53 -0400 Subject: [PATCH 10/16] Blocking in shell functions. Correctly report exceptions in threads. See #196. --- FlatCAMApp.py | 99 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 6626cac9..82ec8dd8 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2059,30 +2059,40 @@ class App(QtCore.QObject): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raiseTclError(str(ex[0])) if status['timed_out']: raise Exception('Timed out!') - def wait_signal2(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" - loop = QtCore.QEventLoop() - signal.connect(loop.quit) - status = {'timed_out': False} - - def report_quit(): - status['timed_out'] = True - loop.quit() - - if timeout is not None: - QtCore.QTimer.singleShot(timeout, report_quit) - loop.exec_() - - if status['timed_out']: - raise Exception('Timed out!') + # def wait_signal2(signal, timeout=10000): + # """Block loop until signal emitted, or timeout (ms) elapses.""" + # loop = QtCore.QEventLoop() + # signal.connect(loop.quit) + # status = {'timed_out': False} + # + # def report_quit(): + # status['timed_out'] = True + # loop.quit() + # + # if timeout is not None: + # QtCore.QTimer.singleShot(timeout, report_quit) + # loop.exec_() + # + # if status['timed_out']: + # raise Exception('Timed out!') def mytest(*args): to = int(args[0]) @@ -2110,24 +2120,45 @@ class App(QtCore.QObject): def mytest2(*args): to = int(args[0]) - try: - for rec in self.recent: - if rec['kind'] == 'gerber': - self.open_gerber(str(rec['filename'])) - break + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break - basename = self.collection.get_names()[0] - isolate(basename, '-passes', '10', '-combine', '1') - iso = self.collection.get_by_name(basename + "_iso") + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") - with wait_signal(self.new_object_available, to): - 1/0 # Force exception - iso.generatecncjob() + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() - return str(self.collection.get_names()) + return str(self.collection.get_names()) - except Exception as e: - return str(e) + def mytest3(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + self.inform.emit("mytest3") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def mytest4(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + 1/0 # Force exception + self.inform.emit("mytest4") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" def import_svg(filename, *args): a, kwa = h(*args) @@ -3239,6 +3270,14 @@ class App(QtCore.QObject): 'fcn': mytest2, 'help': "Test function. Only for testing." }, + 'mytest3': { + 'fcn': mytest3, + 'help': "Test function. Only for testing." + }, + 'mytest4': { + 'fcn': mytest4, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." From b0575a1c34f0c40eb480a884914f679b37e4dc6d Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 24 Mar 2016 15:44:22 -0400 Subject: [PATCH 11/16] Tidying up imports. --- FlatCAMApp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5ca2e90f..538bd141 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from xml.dom.minidom import parseString as parse_xml_string ######################################## ## Imports part of FlatCAM ## @@ -25,7 +26,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool -from xml.dom.minidom import parseString as parse_xml_string + ######################################## ## App ## From a5207294448ce9ff3e9bd811402536479ae26d97 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 24 Mar 2016 16:06:44 -0400 Subject: [PATCH 12/16] Complete implementation of blocking mechanism waiting for signal. See #196. --- FlatCAMApp.py | 32 ++++++++++++++++++++++++++------ FlatCAMWorker.py | 17 ++++++++++------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 538bd141..59335235 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -11,6 +11,7 @@ import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. from xml.dom.minidom import parseString as parse_xml_string +from contextlib import contextmanager ######################################## ## Imports part of FlatCAM ## @@ -106,6 +107,10 @@ class App(QtCore.QObject): message = QtCore.pyqtSignal(str, str, str) + # Emitted when an unhandled exception happens + # in the worker task. + thread_exception = QtCore.pyqtSignal(object) + def __init__(self, user_defaults=True, post_gui=None): """ Starts the application. @@ -2138,13 +2143,22 @@ class App(QtCore.QObject): return a, kwa - from contextlib import contextmanager @contextmanager def wait_signal(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" + """ + Block loop until signal emitted, timeout (ms) elapses + or unhandled exception happens in a thread. + + :param signal: Signal to wait for. + """ loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -2153,17 +2167,23 @@ class App(QtCore.QObject): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + #### Block #### loop.exec_() + + # Restore exception management sys.excepthook = oeh if ex: self.raiseTclError(str(ex[0])) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 528171b6..e97d9d11 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -14,24 +14,27 @@ class Worker(QtCore.QObject): self.name = name def run(self): - # FlatCAMApp.App.log.debug("Worker Started!") + self.app.log.debug("Worker Started!") # Tasks are queued in the event listener. self.app.worker_task.connect(self.do_worker_task) def do_worker_task(self, task): - # FlatCAMApp.App.log.debug("Running task: %s" % str(task)) + self.app.log.debug("Running task: %s" % str(task)) # 'worker_name' property of task allows to target # specific worker. - if 'worker_name' in task and task['worker_name'] == self.name: - task['fcn'](*task['params']) - return + if ('worker_name' in task and task['worker_name'] == self.name) or \ + ('worker_name' not in task and self.name is None): + + try: + task['fcn'](*task['params']) + except Exception as e: + self.app.thread_exception.emit(e) + raise e - if 'worker_name' not in task and self.name is None: - task['fcn'](*task['params']) return # FlatCAMApp.App.log.debug("Task ignored.") From 4a57e437fc4cf4ef03768d8623f242391d10443b Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 12:16:54 +0100 Subject: [PATCH 13/16] Implement shell window as dockable --- FlatCAMApp.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 59335235..0a4873aa 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -476,7 +476,7 @@ class App(QtCore.QObject): self.ui.menuviewdisableall.triggered.connect(self.disable_plots) self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True)) self.ui.menuviewenable.triggered.connect(self.enable_all_plots) - self.ui.menutoolshell.triggered.connect(lambda: self.shell.show()) + self.ui.menutoolshell.triggered.connect(self.on_toggle_shell) self.ui.menuhelp_about.triggered.connect(self.on_about) self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url)) self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url)) @@ -490,7 +490,7 @@ class App(QtCore.QObject): self.ui.editgeo_btn.triggered.connect(self.edit_geometry) self.ui.updategeo_btn.triggered.connect(self.editor2geometry) self.ui.delete_btn.triggered.connect(self.on_delete) - self.ui.shell_btn.triggered.connect(lambda: self.shell.show()) + self.ui.shell_btn.triggered.connect(self.on_toggle_shell) # Object list self.collection.view.activated.connect(self.on_row_activated) # Options @@ -525,14 +525,25 @@ class App(QtCore.QObject): self.shell = FCShell(self) self.shell.setWindowIcon(self.ui.app_icon) self.shell.setWindowTitle("FlatCAM Shell") - if self.defaults["shell_at_startup"]: - self.shell.show() self.shell.resize(*self.defaults["shell_shape"]) self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version) self.shell.append_output("Type help to get started.\n\n") self.tcl = Tkinter.Tcl() self.setup_shell() + + self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") + self.ui.shell_dock.setWidget(self.shell) + self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.ui.shell_dock.setFeatures(QtGui.QDockWidget.DockWidgetMovable | + QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetClosable) + self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) + + if self.defaults["shell_at_startup"]: + self.ui.shell_dock.show() + else: + self.ui.shell_dock.hide() + if self.cmd_line_shellfile: try: with open(self.cmd_line_shellfile, "r") as myfile: @@ -1009,6 +1020,16 @@ class App(QtCore.QObject): if not silent: self.inform.emit("Defaults saved.") + def on_toggle_shell(self): + """ + toggle shell if is visible close it if closed open it + :return: + """ + if self.ui.shell_dock.isVisible(): + self.ui.shell_dock.hide() + else: + self.ui.shell_dock.show() + def on_edit_join(self): """ Callback for Edit->Join. Joins the selected geometry objects into From 5ec25ebea6e10b19d20ba36bdbdbfab0b476fec7 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 12:24:57 +0100 Subject: [PATCH 14/16] remove blank line --- FlatCAMApp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0a4873aa..c510b742 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -531,7 +531,6 @@ class App(QtCore.QObject): self.tcl = Tkinter.Tcl() self.setup_shell() - self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") self.ui.shell_dock.setWidget(self.shell) self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) From 05a9e05c97c3cab7f44d7c3e334864b362ac2c53 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 25 Mar 2016 21:28:02 -0400 Subject: [PATCH 15/16] Removed background highlighting in shell. --- termwidget.py | 61 +++++++++++++++++++-------------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/termwidget.py b/termwidget.py index d6309fd3..36487b6a 100644 --- a/termwidget.py +++ b/termwidget.py @@ -19,13 +19,13 @@ class _ExpandableTextEdit(QTextEdit): historyNext = pyqtSignal() historyPrev = pyqtSignal() - def __init__(self, termWidget, *args): + def __init__(self, termwidget, *args): QTextEdit.__init__(self, *args) self.setStyleSheet("font: 9pt \"Courier\";") self._fittedHeight = 1 self.textChanged.connect(self._fit_to_document) self._fit_to_document() - self._termWidget = termWidget + self._termWidget = termwidget def sizeHint(self): """ @@ -39,10 +39,10 @@ class _ExpandableTextEdit(QTextEdit): """ Update widget height to fit all text """ - documentSize = self.document().size().toSize() - self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height()) + documentsize = self.document().size().toSize() + self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height()) self.setMaximumHeight(self._fittedHeight) - self.updateGeometry(); + self.updateGeometry() def keyPressEvent(self, event): """ @@ -55,26 +55,26 @@ class _ExpandableTextEdit(QTextEdit): return elif event.matches(QKeySequence.MoveToNextLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeEnd = text[cursorPos:] + cursor_pos = self.textCursor().position() + textBeforeEnd = text[cursor_pos:] # if len(textBeforeEnd.splitlines()) <= 1: if len(textBeforeEnd.split('\n')) <= 1: self.historyNext.emit() return elif event.matches(QKeySequence.MoveToPreviousLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeStart = text[:cursorPos] + cursor_pos = self.textCursor().position() + text_before_start = text[:cursor_pos] # lineCount = len(textBeforeStart.splitlines()) - lineCount = len(textBeforeStart.split('\n')) - if len(textBeforeStart) > 0 and \ - (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'): - lineCount += 1 - if lineCount <= 1: + line_count = len(text_before_start.split('\n')) + if len(text_before_start) > 0 and \ + (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'): + line_count += 1 + if line_count <= 1: self.historyPrev.emit() return elif event.matches(QKeySequence.MoveToNextPage) or \ - event.matches(QKeySequence.MoveToPreviousPage): + event.matches(QKeySequence.MoveToPreviousPage): return self._termWidget.browser().keyPressEvent(event) QTextEdit.keyPressEvent(self, event) @@ -94,8 +94,9 @@ class TermWidget(QWidget): self._browser = QTextEdit(self) self._browser.setStyleSheet("font: 9pt \"Courier\";") self._browser.setReadOnly(True) - self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() + - "span {white-space:pre;}") + self._browser.document().setDefaultStyleSheet( + self._browser.document().defaultStyleSheet() + + "span {white-space:pre;}") self._edit = _ExpandableTextEdit(self, self) self._edit.historyNext.connect(self._on_history_next) @@ -120,30 +121,12 @@ class TermWidget(QWidget): assert style in ('in', 'out', 'err') text = cgi.escape(text) - text = text.replace('\n', '
') - if style != 'out': - def_bg = self._browser.palette().color(QPalette.Base) - h, s, v, a = def_bg.getHsvF() - - if style == 'in': - if v > 0.5: # white background - v = v - (v / 8) # make darker - else: - v = v + ((1 - v) / 4) # make ligher - else: # err - if v < 0.5: - v = v + ((1 - v) / 4) # make ligher - - if h == -1: # make red - h = 0 - s = .4 - else: - h = h + ((1 - h) * 0.5) # make more red - - bg = QColor.fromHsvF(h, s, v).name() - text = '%s' % (str(bg), text) + if style == 'in': + text = '%s' % text + elif style == 'err': + text = '%s' % text else: text = '%s' % text # without span
is ignored!!! From b1f2b680e30dff243f258d0891c8b3916d6871b5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 25 Mar 2016 22:00:05 -0400 Subject: [PATCH 16/16] Fixes #198 --- termwidget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/termwidget.py b/termwidget.py index 36487b6a..b2e4fdba 100644 --- a/termwidget.py +++ b/termwidget.py @@ -79,6 +79,10 @@ class _ExpandableTextEdit(QTextEdit): QTextEdit.keyPressEvent(self, event) + def insertFromMimeData(self, mime_data): + # Paste only plain text. + self.insertPlainText(mime_data.text()) + class TermWidget(QWidget): """