Ver código fonte

merge new pull requests from FlatCAM->master
implement executing of tasks inside worker thread
cleanups, reimplement Isolate/New/OpenGerber as OOP style Shell commands
disable edit during shell execution, show some progress
add ability for breakpoints in other threads and only if available
add X11 safe flag, not sure what happen on windows

Kamil Sopko 9 anos atrás
pai
commit
e96ee1af29

+ 5 - 0
FlatCAM.py

@@ -1,5 +1,6 @@
 import sys
 import sys
 from PyQt4 import QtGui
 from PyQt4 import QtGui
+from PyQt4 import QtCore
 from FlatCAMApp import App
 from FlatCAMApp import App
 
 
 def debug_trace():
 def debug_trace():
@@ -10,6 +11,10 @@ def debug_trace():
     #set_trace()
     #set_trace()
 
 
 debug_trace()
 debug_trace()
+
+# all X11 calling should  be thread safe  otherwise we have  strenght issues
+QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
+
 app = QtGui.QApplication(sys.argv)
 app = QtGui.QApplication(sys.argv)
 fc = App()
 fc = App()
 sys.exit(app.exec_())
 sys.exit(app.exec_())

+ 222 - 22
FlatCAMApp.py

@@ -10,6 +10,7 @@ import os
 import Tkinter
 import Tkinter
 from PyQt4 import QtCore
 from PyQt4 import QtCore
 import time  # Just used for debugging. Double check before removing.
 import time  # Just used for debugging. Double check before removing.
+from contextlib import contextmanager
 
 
 ########################################
 ########################################
 ##      Imports part of FlatCAM       ##
 ##      Imports part of FlatCAM       ##
@@ -25,6 +26,7 @@ from FlatCAMDraw import FlatCAMDraw
 from FlatCAMProcess import *
 from FlatCAMProcess import *
 from MeasurementTool import Measurement
 from MeasurementTool import Measurement
 from DblSidedTool import DblSidedTool
 from DblSidedTool import DblSidedTool
+from xml.dom.minidom import parseString as parse_xml_string
 import tclCommands
 import tclCommands
 
 
 ########################################
 ########################################
@@ -103,6 +105,9 @@ class App(QtCore.QObject):
     # and is ready to be used.
     # and is ready to be used.
     new_object_available = QtCore.pyqtSignal(object)
     new_object_available = QtCore.pyqtSignal(object)
 
 
+    # Emmited when shell command is finished(one command only)
+    shell_command_finished = QtCore.pyqtSignal(object)
+
     message = QtCore.pyqtSignal(str, str, str)
     message = QtCore.pyqtSignal(str, str, str)
 
 
     def __init__(self, user_defaults=True, post_gui=None):
     def __init__(self, user_defaults=True, post_gui=None):
@@ -451,6 +456,7 @@ class App(QtCore.QObject):
         self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode)
         self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode)
         self.ui.menufileopenproject.triggered.connect(self.on_file_openproject)
         self.ui.menufileopenproject.triggered.connect(self.on_file_openproject)
         self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg)
         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.menufilesaveproject.triggered.connect(self.on_file_saveproject)
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
@@ -523,8 +529,8 @@ class App(QtCore.QObject):
         self.shell.resize(*self.defaults["shell_shape"])
         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("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.shell.append_output("Type help to get started.\n\n")
-        self.tcl = Tkinter.Tcl()
-        self.setup_shell()
+
+        self.init_tcl()
 
 
         if self.cmd_line_shellfile:
         if self.cmd_line_shellfile:
             try:
             try:
@@ -542,6 +548,17 @@ class App(QtCore.QObject):
 
 
         App.log.debug("END of constructor. Releasing control.")
         App.log.debug("END of constructor. Releasing control.")
 
 
+    def init_tcl(self):
+        if hasattr(self,'tcl'):
+            # self.tcl = None
+            # TODO  we need  to clean  non default variables and procedures here
+            # new object cannot be used here as it  will not remember values created for next passes,
+            # because tcl  was execudted in old instance of TCL
+            pass
+        else:
+            self.tcl = Tkinter.Tcl()
+            self.setup_shell()
+
     def defaults_read_form(self):
     def defaults_read_form(self):
         for option in self.defaults_form_fields:
         for option in self.defaults_form_fields:
             self.defaults[option] = self.defaults_form_fields[option].get_value()
             self.defaults[option] = self.defaults_form_fields[option].get_value()
@@ -676,12 +693,16 @@ class App(QtCore.QObject):
     def exec_command(self, text):
     def exec_command(self, text):
         """
         """
         Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
         Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+        Also handles execution in separated threads
 
 
         :param text:
         :param text:
         :return: output if there was any
         :return: output if there was any
         """
         """
 
 
-        return self.exec_command_test(text, False)
+        self.report_usage('exec_command')
+
+        result = self.exec_command_test(text, False)
+        return result
 
 
     def exec_command_test(self, text, reraise=True):
     def exec_command_test(self, text, reraise=True):
         """
         """
@@ -692,11 +713,10 @@ class App(QtCore.QObject):
         :return: output if there was any
         :return: output if there was any
         """
         """
 
 
-        self.report_usage('exec_command')
-
         text = str(text)
         text = str(text)
 
 
         try:
         try:
+            self.shell.open_proccessing()
             result = self.tcl.eval(str(text))
             result = self.tcl.eval(str(text))
             if result!='None':
             if result!='None':
                 self.shell.append_output(result + '\n')
                 self.shell.append_output(result + '\n')
@@ -708,6 +728,9 @@ class App(QtCore.QObject):
             #show error in console and just return or in test raise exception
             #show error in console and just return or in test raise exception
             if reraise:
             if reraise:
                 raise e
                 raise e
+        finally:
+            self.shell.close_proccessing()
+            pass
         return result
         return result
 
 
         """
         """
@@ -1491,6 +1514,9 @@ class App(QtCore.QObject):
 
 
         self.plotcanvas.clear()
         self.plotcanvas.clear()
 
 
+        # tcl needs to be reinitialized, otherwise  old shell variables etc  remains
+        self.init_tcl()
+
         self.collection.delete_all()
         self.collection.delete_all()
 
 
         self.setup_component_editor()
         self.setup_component_editor()
@@ -1612,6 +1638,53 @@ class App(QtCore.QObject):
             # thread safe. The new_project()
             # thread safe. The new_project()
             self.open_project(filename)
             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
+
+        # Check for more compatible types and add as required
+        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)
+            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):
     def on_file_importsvg(self):
         """
         """
         Callback for menu item File->Import SVG.
         Callback for menu item File->Import SVG.
@@ -1696,6 +1769,51 @@ class App(QtCore.QObject):
         else:
         else:
             self.inform.emit("Project copy saved to: " + self.project_filename)
             self.inform.emit("Project copy saved to: " + self.project_filename)
 
 
+
+    def export_svg(self, obj_name, filename, scale_factor=0.00):
+        """
+        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
+
+        with self.proc_container.new("Exporting SVG") as proc:
+            exported_svg = obj.export_svg(scale_factor=scale_factor)
+
+            # Determine bounding area for svg export
+            bounds = obj.bounds()
+            size = obj.size()
+
+            # Convert everything to strings for use in the xml doc
+            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
+            # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+            svg_elem = svg_header + exported_svg + svg_footer
+
+            # Parse the xml through a xml parser just to add line feeds and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            with open(filename, 'w') as fp:
+                fp.write(doc.toprettyxml())
+
     def import_svg(self, filename, outname=None):
     def import_svg(self, filename, outname=None):
         """
         """
         Adds a new Geometry Object to the projects and populates
         Adds a new Geometry Object to the projects and populates
@@ -2079,7 +2197,7 @@ class App(QtCore.QObject):
 
 
             return a, kwa
             return a, kwa
 
 
-        from contextlib import contextmanager
+
         @contextmanager
         @contextmanager
         def wait_signal(signal, timeout=10000):
         def wait_signal(signal, timeout=10000):
             """Block loop until signal emitted, or timeout (ms) elapses."""
             """Block loop until signal emitted, or timeout (ms) elapses."""
@@ -2094,30 +2212,40 @@ class App(QtCore.QObject):
 
 
             yield
             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:
             if timeout is not None:
                 QtCore.QTimer.singleShot(timeout, report_quit)
                 QtCore.QTimer.singleShot(timeout, report_quit)
 
 
             loop.exec_()
             loop.exec_()
+            sys.excepthook = oeh
+            if ex:
+                self.raiseTclError(str(ex[0]))
 
 
             if status['timed_out']:
             if status['timed_out']:
                 raise Exception('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):
         def mytest(*args):
             to = int(args[0])
             to = int(args[0])
@@ -2142,8 +2270,60 @@ class App(QtCore.QObject):
             except Exception as e:
             except Exception as e:
                 return str(e)
                 return str(e)
 
 
+        def mytest2(*args):
+            to = int(args[0])
+
+            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())
             return str(self.collection.get_names())
 
 
+        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 export_svg(name, filename, *args):
+            a, kwa = h(*args)
+            types = {'scale_factor': float}
+
+            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):
         def import_svg(filename, *args):
             a, kwa = h(*args)
             a, kwa = h(*args)
             types = {'outname': str}
             types = {'outname': str}
@@ -3274,6 +3454,18 @@ class App(QtCore.QObject):
                 'fcn': mytest,
                 'fcn': mytest,
                 'help': "Test function. Only for testing."
                 'help': "Test function. Only for testing."
             },
             },
+            'mytest2': {
+                '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': {
             'help': {
                 'fcn': shelp,
                 'fcn': shelp,
                 'help': "Shows list of commands."
                 'help': "Shows list of commands."
@@ -3284,6 +3476,14 @@ class App(QtCore.QObject):
                         "> import_svg <filename>" +
                         "> import_svg <filename>" +
                         "   filename: Path to the file to import."
                         "   filename: Path to the file to import."
             },
             },
+            'export_svg': {
+                'fcn': export_svg,
+                'help': "Export a Geometry Object as a SVG File\n" +
+                        "> export_svg <name> <filename> [-scale_factor <0.0 (float)>]\n" +
+                        "   name: Name of the geometry object to export.\n" +
+                        "   filename: Path to the file to export.\n" +
+                        "   scale_factor: Multiplication factor used for scaling line widths during export."
+            },
             'open_gerber': {
             'open_gerber': {
                 'fcn': open_gerber,
                 'fcn': open_gerber,
                 'help': "Opens a Gerber file.\n"
                 'help': "Opens a Gerber file.\n"

+ 4 - 0
FlatCAMGUI.py

@@ -48,6 +48,10 @@ class FlatCAMGUI(QtGui.QMainWindow):
         self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self)
         self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self)
         self.menufile.addAction(self.menufileimportsvg)
         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
         # Save Project
         self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self)
         self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self)
         self.menufile.addAction(self.menufilesaveproject)
         self.menufile.addAction(self.menufilesaveproject)

+ 1 - 1
FlatCAMShell.py

@@ -22,4 +22,4 @@ class FCShell(termwidget.TermWidget):
         return True
         return True
 
 
     def child_exec_command(self, text):
     def child_exec_command(self, text):
-        self._sysShell.exec_command(text)
+        self._sysShell.exec_command(text)

+ 25 - 3
FlatCAMWorker.py

@@ -1,6 +1,4 @@
 from PyQt4 import QtCore
 from PyQt4 import QtCore
-#import FlatCAMApp
-
 
 
 class Worker(QtCore.QObject):
 class Worker(QtCore.QObject):
     """
     """
@@ -8,12 +6,33 @@ class Worker(QtCore.QObject):
     in a single independent thread.
     in a single independent thread.
     """
     """
 
 
+    # avoid multiple tests  for debug availability
+    pydef_failed = False
+
     def __init__(self, app, name=None):
     def __init__(self, app, name=None):
         super(Worker, self).__init__()
         super(Worker, self).__init__()
         self.app = app
         self.app = app
         self.name = name
         self.name = name
 
 
+    def allow_debug(self):
+        """
+         allow debuging/breakpoints in this threads
+         should work from PyCharm and PyDev
+        :return:
+        """
+
+        if not self.pydef_failed:
+            try:
+                import pydevd
+                pydevd.settrace(suspend=False, trace_only_current_thread=True)
+            except ImportError:
+                pass
+
     def run(self):
     def run(self):
+
+        # allow  debuging/breakpoints in this threads
+        #pydevd.settrace(suspend=False, trace_only_current_thread=True)
+
         # FlatCAMApp.App.log.debug("Worker Started!")
         # FlatCAMApp.App.log.debug("Worker Started!")
         self.app.log.debug("Worker Started!")
         self.app.log.debug("Worker Started!")
 
 
@@ -21,9 +40,12 @@ class Worker(QtCore.QObject):
         self.app.worker_task.connect(self.do_worker_task)
         self.app.worker_task.connect(self.do_worker_task)
 
 
     def do_worker_task(self, task):
     def do_worker_task(self, task):
+
         # FlatCAMApp.App.log.debug("Running task: %s" % str(task))
         # FlatCAMApp.App.log.debug("Running task: %s" % str(task))
         self.app.log.debug("Running task: %s" % str(task))
         self.app.log.debug("Running task: %s" % str(task))
 
 
+        self.allow_debug()
+
         # 'worker_name' property of task allows to target
         # 'worker_name' property of task allows to target
         # specific worker.
         # specific worker.
         if 'worker_name' in task and task['worker_name'] == self.name:
         if 'worker_name' in task and task['worker_name'] == self.name:
@@ -35,4 +57,4 @@ class Worker(QtCore.QObject):
             return
             return
 
 
         # FlatCAMApp.App.log.debug("Task ignored.")
         # FlatCAMApp.App.log.debug("Task ignored.")
-        self.app.log.debug("Task ignored.")
+        self.app.log.debug("Task ignored.")

+ 68 - 0
camlib.py

@@ -890,6 +890,26 @@ class Geometry(object):
         """
         """
         self.solid_geometry = [cascaded_union(self.solid_geometry)]
         self.solid_geometry = [cascaded_union(self.solid_geometry)]
 
 
+    def export_svg(self, scale_factor=0.00):
+        """
+        Exports the Gemoetry Object as a SVG Element
+
+        :return: SVG Element
+        """
+        # 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=scale_factor)
+        return svg_elem
 
 
 class ApertureMacro:
 class ApertureMacro:
     """
     """
@@ -3334,6 +3354,54 @@ class CNCjob(Geometry):
 
 
         self.create_geometry()
         self.create_geometry()
 
 
+    def export_svg(self, scale_factor=0.00):
+        """
+        Exports the CNC Job as a 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
+
+        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
+        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 the overall board size
+        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
+        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 = ""
+        if travels:
+            svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D")
+        if cuts:
+            svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF")
+
+        return svg_elem
 
 
 # def get_bounds(geometry_set):
 # def get_bounds(geometry_set):
 #     xmin = Inf
 #     xmin = Inf

+ 47 - 6
tclCommands/TclCommand.py

@@ -1,3 +1,4 @@
+import sys
 import re
 import re
 import FlatCAMApp
 import FlatCAMApp
 import abc
 import abc
@@ -41,6 +42,9 @@ class TclCommand(object):
         'examples': []
         'examples': []
     }
     }
 
 
+    # original incoming arguments into command
+    original_args = None
+
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
         if self.app is None:
         if self.app is None:
@@ -59,6 +63,18 @@ class TclCommand(object):
 
 
         self.app.raise_tcl_error(text)
         self.app.raise_tcl_error(text)
 
 
+    def get_current_command(self):
+        """
+        get current command, we are not able to get it from TCL we have to reconstruct it
+        :return: current command
+        """
+        command_string = []
+        command_string.append(self.aliases[0])
+        if self.original_args is not None:
+            for arg in self.original_args:
+                command_string.append(arg)
+        return " ".join(command_string)
+
     def get_decorated_help(self):
     def get_decorated_help(self):
         """
         """
         Decorate help for TCL console output.
         Decorate help for TCL console output.
@@ -176,7 +192,7 @@ class TclCommand(object):
 
 
         # check options
         # check options
         for key in options:
         for key in options:
-            if key not in self.option_types:
+            if key not in self.option_types and  key is not 'timeout':
                 self.raise_tcl_error('Unknown parameter: %s' % key)
                 self.raise_tcl_error('Unknown parameter: %s' % key)
             try:
             try:
                 named_args[key] = self.option_types[key](options[key])
                 named_args[key] = self.option_types[key](options[key])
@@ -201,8 +217,11 @@ class TclCommand(object):
         :return: None, output text or exception
         :return: None, output text or exception
         """
         """
 
 
+        #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
+
         try:
         try:
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
+            self.original_args=args
             args, unnamed_args = self.check_args(args)
             args, unnamed_args = self.check_args(args)
             return self.execute(args, unnamed_args)
             return self.execute(args, unnamed_args)
         except Exception as unknown:
         except Exception as unknown:
@@ -239,9 +258,15 @@ class TclCommandSignaled(TclCommand):
         it handles  all neccessary stuff about blocking and passing exeptions
         it handles  all neccessary stuff about blocking and passing exeptions
     """
     """
 
 
-    # default  timeout for operation is  30 sec, but it can be much more
-    default_timeout = 30000
+    # default  timeout for operation is  300 sec, but it can be much more
+    default_timeout = 300000
+
+    output = None
 
 
+    def execute_call(self, args, unnamed_args):
+
+        self.output = self.execute(args, unnamed_args)
+        self.app.shell_command_finished.emit(self)
 
 
     def execute_wrapper(self, *args):
     def execute_wrapper(self, *args):
         """
         """
@@ -254,7 +279,7 @@ class TclCommandSignaled(TclCommand):
         """
         """
 
 
         @contextmanager
         @contextmanager
-        def wait_signal(signal, timeout=30000):
+        def wait_signal(signal, timeout=300000):
             """Block loop until signal emitted, or timeout (ms) elapses."""
             """Block loop until signal emitted, or timeout (ms) elapses."""
             loop = QtCore.QEventLoop()
             loop = QtCore.QEventLoop()
             signal.connect(loop.quit)
             signal.connect(loop.quit)
@@ -267,27 +292,43 @@ class TclCommandSignaled(TclCommand):
 
 
             yield
             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:
             if timeout is not None:
                 QtCore.QTimer.singleShot(timeout, report_quit)
                 QtCore.QTimer.singleShot(timeout, report_quit)
 
 
             loop.exec_()
             loop.exec_()
 
 
+            sys.excepthook = oeh
+            if ex:
+                self.raise_tcl_error(str(ex[0]))
+
             if status['timed_out']:
             if status['timed_out']:
                 self.app.raise_tcl_unknown_error('Operation timed out!')
                 self.app.raise_tcl_unknown_error('Operation timed out!')
 
 
         try:
         try:
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
+            self.original_args=args
             args, unnamed_args = self.check_args(args)
             args, unnamed_args = self.check_args(args)
             if 'timeout' in args:
             if 'timeout' in args:
                 passed_timeout=args['timeout']
                 passed_timeout=args['timeout']
                 del args['timeout']
                 del args['timeout']
             else:
             else:
                 passed_timeout=self.default_timeout
                 passed_timeout=self.default_timeout
-            with wait_signal(self.app.new_object_available, passed_timeout):
+
+            # set detail for processing, it will be there until next open or close
+            self.app.shell.open_proccessing(self.get_current_command())
+
+            with wait_signal(self.app.shell_command_finished, passed_timeout):
                 # every TclCommandNewObject ancestor  support  timeout as parameter,
                 # every TclCommandNewObject ancestor  support  timeout as parameter,
                 # but it does not mean anything for child itself
                 # but it does not mean anything for child itself
                 # when operation  will be  really long is good  to set it higher then defqault 30s
                 # when operation  will be  really long is good  to set it higher then defqault 30s
-                return self.execute(args, unnamed_args)
+                self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
 
 
         except Exception as unknown:
         except Exception as unknown:
             self.log.error("TCL command '%s' failed." % str(self))
             self.log.error("TCL command '%s' failed." % str(self))

+ 1 - 1
tclCommands/TclCommandAddPolygon.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandAddPolygon(TclCommand.TclCommand):
+class TclCommandAddPolygon(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to create a polygon in the given Geometry object
     Tcl shell command to create a polygon in the given Geometry object
     """
     """

+ 1 - 1
tclCommands/TclCommandAddPolyline.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandAddPolyline(TclCommand.TclCommand):
+class TclCommandAddPolyline(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to create a polyline in the given Geometry object
     Tcl shell command to create a polyline in the given Geometry object
     """
     """

+ 1 - 6
tclCommands/TclCommandCncjob.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandCncjob(TclCommand.TclCommand):
+class TclCommandCncjob(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to Generates a CNC Job from a Geometry Object.
     Tcl shell command to Generates a CNC Job from a Geometry Object.
 
 
@@ -70,11 +70,6 @@ class TclCommandCncjob(TclCommand.TclCommand):
         if 'outname' not in args:
         if 'outname' not in args:
             args['outname'] = name + "_cnc"
             args['outname'] = name + "_cnc"
 
 
-        if 'timeout' in args:
-            timeout = args['timeout']
-        else:
-            timeout = 10000
-
         obj = self.app.collection.get_by_name(name)
         obj = self.app.collection.get_by_name(name)
         if obj is None:
         if obj is None:
             self.raise_tcl_error("Object not found: %s" % name)
             self.raise_tcl_error("Object not found: %s" % name)

+ 1 - 1
tclCommands/TclCommandExportGcode.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandExportGcode(TclCommand.TclCommand):
+class TclCommandExportGcode(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to export gcode as  tcl output for "set X [export_gcode ...]"
     Tcl shell command to export gcode as  tcl output for "set X [export_gcode ...]"
 
 

+ 2 - 2
tclCommands/TclCommandExteriors.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandExteriors(TclCommand.TclCommand):
+class TclCommandExteriors(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to get exteriors of polygons
     Tcl shell command to get exteriors of polygons
     """
     """
@@ -57,7 +57,7 @@ class TclCommandExteriors(TclCommand.TclCommand):
         if not isinstance(obj, Geometry):
         if not isinstance(obj, Geometry):
             self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
             self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
 
 
-        def geo_init(geo_obj):
+        def geo_init(geo_obj, app_obj):
             geo_obj.solid_geometry = obj_exteriors
             geo_obj.solid_geometry = obj_exteriors
 
 
         obj_exteriors = obj.get_exteriors()
         obj_exteriors = obj.get_exteriors()

+ 2 - 2
tclCommands/TclCommandInteriors.py

@@ -2,7 +2,7 @@ from ObjectCollection import *
 import TclCommand
 import TclCommand
 
 
 
 
-class TclCommandInteriors(TclCommand.TclCommand):
+class TclCommandInteriors(TclCommand.TclCommandSignaled):
     """
     """
     Tcl shell command to get interiors of polygons
     Tcl shell command to get interiors of polygons
     """
     """
@@ -57,7 +57,7 @@ class TclCommandInteriors(TclCommand.TclCommand):
         if not isinstance(obj, Geometry):
         if not isinstance(obj, Geometry):
             self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
             self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
 
 
-        def geo_init(geo_obj):
+        def geo_init(geo_obj, app_obj):
             geo_obj.solid_geometry = obj_exteriors
             geo_obj.solid_geometry = obj_exteriors
 
 
         obj_exteriors = obj.get_interiors()
         obj_exteriors = obj.get_interiors()

+ 79 - 0
tclCommands/TclCommandIsolate.py

@@ -0,0 +1,79 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandIsolate(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to Creates isolation routing geometry for the given Gerber.
+
+    example:
+        set_sys units MM
+        new
+        open_gerber tests/gerber_files/simple1.gbr -outname margin
+        isolate margin -dia 3
+        cncjob margin_iso
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['isolate']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('dia',float),
+        ('passes',int),
+        ('overlap',float),
+        ('combine',int),
+        ('outname',str)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Creates isolation routing geometry for the given Gerber.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the source object.'),
+            ('dia', 'Tool diameter.'),
+            ('passes', 'Passes of tool width.'),
+            ('overlap', 'Fraction of tool diameter to overlap passes.'),
+            ('combine', 'Combine all passes into one geometry.'),
+            ('outname', 'Name of the resulting Geometry object.')
+        ]),
+        'examples': []
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'outname' not in args:
+            args['outname'] = name + "_iso"
+
+        if 'timeout' in args:
+            timeout = args['timeout']
+        else:
+            timeout = 10000
+
+        obj = self.app.collection.get_by_name(name)
+        if obj is None:
+            self.raise_tcl_error("Object not found: %s" % name)
+
+        if not isinstance(obj, FlatCAMGerber):
+            self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj)))
+
+        del args['name']
+        obj.isolate(**args)

+ 40 - 0
tclCommands/TclCommandNew.py

@@ -0,0 +1,40 @@
+from ObjectCollection import *
+from PyQt4 import QtCore
+import TclCommand
+
+
+class TclCommandNew(TclCommand.TclCommand):
+    """
+    Tcl shell command to starts a new project. Clears objects from memory
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['new']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict()
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict()
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = []
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Starts a new project. Clears objects from memory.",
+        'args':  collections.OrderedDict(),
+        'examples': []
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        self.app.on_file_new()

+ 95 - 0
tclCommands/TclCommandOpenGerber.py

@@ -0,0 +1,95 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandOpenGerber(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to opens a Gerber file
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['open_gerber']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('filename', str)
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('follow', str),
+        ('outname', str)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['filename']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Opens a Gerber file.",
+        'args':  collections.OrderedDict([
+            ('filename', 'Path to file to open.'),
+            ('follow', 'N If 1, does not create polygons, just follows the gerber path.'),
+            ('outname', 'Name of the resulting Geometry object.')
+        ]),
+        'examples': []
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        # How the object should be initialized
+        def obj_init(gerber_obj, app_obj):
+
+            if not isinstance(gerber_obj, Geometry):
+                self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (outname, type(gerber_obj)))
+
+            # Opening the file happens here
+            self.app.progress.emit(30)
+            try:
+                gerber_obj.parse_file(filename, follow=follow)
+
+            except IOError:
+                app_obj.inform.emit("[error] Failed to open file: %s " % filename)
+                app_obj.progress.emit(0)
+                self.raise_tcl_error('Failed to open file: %s' % filename)
+
+            except ParseError, e:
+                app_obj.inform.emit("[error] Failed to parse file: %s, %s " % (filename, str(e)))
+                app_obj.progress.emit(0)
+                self.log.error(str(e))
+                raise
+
+            # Further parsing
+            app_obj.progress.emit(70)
+
+        filename = args['filename']
+
+        if 'outname' in args:
+            outname = args['outname']
+        else:
+            outname = filename.split('/')[-1].split('\\')[-1]
+
+        follow = None
+        if 'follow' in args:
+            follow = args['follow']
+
+        with self.app.proc_container.new("Opening Gerber"):
+
+            # Object creation
+            self.app.new_object("gerber", outname, obj_init)
+
+            # Register recent file
+            self.app.file_opened.emit("gerber", filename)
+
+            self.app.progress.emit(100)
+
+            # GUI feedback
+            self.app.inform.emit("Opened: " + filename)

+ 7 - 3
tclCommands/__init__.py

@@ -2,12 +2,16 @@ import pkgutil
 import sys
 import sys
 
 
 # allowed command modules
 # allowed command modules
-import tclCommands.TclCommandExteriors
-import tclCommands.TclCommandInteriors
 import tclCommands.TclCommandAddPolygon
 import tclCommands.TclCommandAddPolygon
 import tclCommands.TclCommandAddPolyline
 import tclCommands.TclCommandAddPolyline
-import tclCommands.TclCommandExportGcode
 import tclCommands.TclCommandCncjob
 import tclCommands.TclCommandCncjob
+import tclCommands.TclCommandExportGcode
+import tclCommands.TclCommandExteriors
+import tclCommands.TclCommandInteriors
+import tclCommands.TclCommandIsolate
+import tclCommands.TclCommandNew
+import tclCommands.TclCommandOpenGerber
+
 
 
 __all__ = []
 __all__ = []
 
 

+ 27 - 1
termwidget.py

@@ -4,7 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history.
 """
 """
 
 
 import cgi
 import cgi
-
+from PyQt4 import QtCore
 from PyQt4.QtCore import pyqtSignal
 from PyQt4.QtCore import pyqtSignal
 from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
 from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
                         QSizePolicy, QTextCursor, QTextEdit, \
                         QSizePolicy, QTextCursor, QTextEdit, \
@@ -113,6 +113,32 @@ class TermWidget(QWidget):
 
 
         self._edit.setFocus()
         self._edit.setFocus()
 
 
+    def open_proccessing(self, detail=None):
+        """
+        Open processing and disable using shell commands  again until all commands are finished
+        :return:
+        """
+
+        self._edit.setTextColor(QtCore.Qt.white)
+        self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen)
+        if detail is None:
+            self._edit.setPlainText("...proccessing...")
+        else:
+            self._edit.setPlainText("...proccessing... [%s]" % detail)
+
+        self._edit.setDisabled(True)
+
+    def close_proccessing(self):
+        """
+        Close processing and enable using shell commands  again
+        :return:
+        """
+
+        self._edit.setTextColor(QtCore.Qt.black)
+        self._edit.setTextBackgroundColor(QtCore.Qt.white)
+        self._edit.setPlainText('')
+        self._edit.setDisabled(False)
+
     def _append_to_browser(self, style, text):
     def _append_to_browser(self, style, text):
         """
         """
         Convert text to HTML for inserting it to browser
         Convert text to HTML for inserting it to browser

+ 23 - 1
tests/test_tcl_shell.py

@@ -8,7 +8,7 @@ from time import sleep
 import os
 import os
 import tempfile
 import tempfile
 
 
-class TclShellCommandTest(unittest.TestCase):
+class TclShellTest(unittest.TestCase):
 
 
     gerber_files = 'tests/gerber_files'
     gerber_files = 'tests/gerber_files'
     copper_bottom_filename = 'detector_copper_bottom.gbr'
     copper_bottom_filename = 'detector_copper_bottom.gbr'
@@ -30,12 +30,21 @@ class TclShellCommandTest(unittest.TestCase):
         # user-defined defaults).
         # user-defined defaults).
         self.fc = App(user_defaults=False)
         self.fc = App(user_defaults=False)
 
 
+        self.fc.shell.show()
+        pass
+
     def tearDown(self):
     def tearDown(self):
+        self.app.closeAllWindows()
+
         del self.fc
         del self.fc
         del self.app
         del self.app
+        pass
 
 
     def test_set_get_units(self):
     def test_set_get_units(self):
 
 
+        self.fc.exec_command_test('set_sys units MM')
+        self.fc.exec_command_test('new')
+
         self.fc.exec_command_test('set_sys units IN')
         self.fc.exec_command_test('set_sys units IN')
         self.fc.exec_command_test('new')
         self.fc.exec_command_test('new')
         units=self.fc.exec_command_test('get_sys units')
         units=self.fc.exec_command_test('get_sys units')
@@ -46,6 +55,7 @@ class TclShellCommandTest(unittest.TestCase):
         units=self.fc.exec_command_test('get_sys units')
         units=self.fc.exec_command_test('get_sys units')
         self.assertEquals(units, "MM")
         self.assertEquals(units, "MM")
 
 
+
     def test_gerber_flow(self):
     def test_gerber_flow(self):
 
 
         # open  gerber files top, bottom and cutout
         # open  gerber files top, bottom and cutout
@@ -139,9 +149,21 @@ class TclShellCommandTest(unittest.TestCase):
 
 
         # TODO: tests for tcl
         # TODO: tests for tcl
 
 
+    def test_open_gerber(self):
+
+        self.fc.exec_command_test('set_sys units MM')
+        self.fc.exec_command_test('new')
+
+        self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
+        gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
+        self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
+                        "Expected FlatCAMGerber, instead, %s is %s" %
+                        (self.gerber_top_name, type(gerber_top_obj)))
+
     def test_excellon_flow(self):
     def test_excellon_flow(self):
 
 
         self.fc.exec_command_test('set_sys units MM')
         self.fc.exec_command_test('set_sys units MM')
+        self.fc.exec_command_test('new')
         self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name))
         self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name))
         excellon_obj = self.fc.collection.get_by_name(self.excellon_name)
         excellon_obj = self.fc.collection.get_by_name(self.excellon_name)
         self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),
         self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),