Просмотр исходного кода

Merge branch 'master' of https://bitbucket.org/jpcgt/flatcam

Juan Pablo Caram 9 лет назад
Родитель
Сommit
3f7e4a5966

+ 5 - 0
FlatCAM.py

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

+ 358 - 234
FlatCAMApp.py

@@ -1,4 +1,4 @@
-import sys
+import sys, traceback
 import urllib
 import getopt
 import random
@@ -27,7 +27,7 @@ from FlatCAMDraw import FlatCAMDraw
 from FlatCAMProcess import *
 from MeasurementTool import Measurement
 from DblSidedTool import DblSidedTool
-
+import tclCommands
 
 ########################################
 ##                App                 ##
@@ -107,6 +107,9 @@ class App(QtCore.QObject):
 
     message = QtCore.pyqtSignal(str, str, str)
 
+    # Emmited when shell command is finished(one command only)
+    shell_command_finished = QtCore.pyqtSignal(object)
+
     # Emitted when an unhandled exception happens
     # in the worker task.
     thread_exception = QtCore.pyqtSignal(object)
@@ -283,6 +286,8 @@ class App(QtCore.QObject):
             "cncjob_tooldia": 0.016,
             "cncjob_prepend": "",
             "cncjob_append": "",
+            "background_timeout": 300000, #default value is 5 minutes
+            "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future).
 
             # Persistence
             "last_folder": None,
@@ -528,8 +533,8 @@ class App(QtCore.QObject):
         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.init_tcl()
 
         self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell")
         self.ui.shell_dock.setWidget(self.shell)
@@ -559,6 +564,17 @@ class App(QtCore.QObject):
 
         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):
         for option in self.defaults_form_fields:
             self.defaults[option] = self.defaults_form_fields[option].get_value()
@@ -661,36 +677,111 @@ class App(QtCore.QObject):
         else:
             self.defaults['stats'][resource] = 1
 
-    def raiseTclError(self, text):
+    class TclErrorException(Exception):
+        """
+        this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command
+        """
+        pass
+
+    def raise_tcl_unknown_error(self, unknownException):
+        """
+        raise Exception if is different type  than TclErrorException
+        this is here mainly to show unknown errors inside TCL shell console
+        :param unknownException:
+        :return:
+        """
+
+        if not isinstance(unknownException, self.TclErrorException):
+            self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+        else:
+            raise unknownException
+
+    def display_tcl_error(self, error, error_info=None):
+        """
+        escape bracket [ with \  otherwise there is error
+        "ERROR: missing close-bracket" instead of real error
+        :param error: it may be text  or exception
+        :return: None
+        """
+
+        if isinstance(error, Exception):
+
+            exc_type, exc_value, exc_traceback = error_info
+            if not isinstance(error, self.TclErrorException):
+                show_trace = 1
+            else:
+                show_trace = int(self.defaults['verbose_error_level'])
+
+            if show_trace > 0:
+                trc=traceback.format_list(traceback.extract_tb(exc_traceback))
+                trc_formated=[]
+                for a in reversed(trc):
+                    trc_formated.append(a.replace("    ", " > ").replace("\n",""))
+                text="%s\nPython traceback: %s\n%s" % (exc_value,
+                                 exc_type,
+                                 "\n".join(trc_formated))
+
+            else:
+                text="%s" % error
+        else:
+            text=error
+
+        text = text.replace('[', '\\[').replace('"','\\"')
+
+        self.tcl.eval('return -code error "%s"' % text)
+
+    def raise_tcl_error(self, text):
         """
         this method  pass exception from python into TCL as error, so we get stacktrace and reason
         :param text: text of error
         :return: raise exception
         """
-        self.tcl.eval('return -code error "%s"' % text)
-        raise Exception(text)
+
+        self.display_tcl_error(text)
+        raise self.TclErrorException(text)
 
     def exec_command(self, text):
         """
         Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+        Also handles execution in separated threads
 
-        :param text: Input command
-        :return: None
+        :param text:
+        :return: output if there was any
         """
+
         self.report_usage('exec_command')
 
+        result = self.exec_command_test(text, False)
+        return result
+
+    def exec_command_test(self, text, reraise=True):
+        """
+        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+
+        :param text: Input command
+        :param reraise: raise exception and not hide it, used mainly in unittests
+        :return: output if there was any
+        """
+
         text = str(text)
 
         try:
+            self.shell.open_proccessing()
             result = self.tcl.eval(str(text))
-            self.shell.append_output(result + '\n')
+            if result!='None':
+                self.shell.append_output(result + '\n')
         except Tkinter.TclError, e:
             #this will display more precise answer if something in  TCL shell fail
             result = self.tcl.eval("set errorInfo")
             self.log.error("Exec command Exception: %s" % (result + '\n'))
             self.shell.append_error('ERROR: ' + result + '\n')
-            #show error in console and just return
-        return
+            #show error in console and just return or in test raise exception
+            if reraise:
+                raise e
+        finally:
+            self.shell.close_proccessing()
+            pass
+        return result
 
         """
         Code below is unsused. Saved for later.
@@ -1024,6 +1115,7 @@ class App(QtCore.QObject):
         toggle shell if is  visible close it if  closed open it
         :return:
         """
+
         if self.ui.shell_dock.isVisible():
             self.ui.shell_dock.hide()
         else:
@@ -1036,6 +1128,7 @@ class App(QtCore.QObject):
 
         :return: None
         """
+
         objs = self.collection.get_selected()
 
         def initialize(obj, app):
@@ -1483,6 +1576,9 @@ class App(QtCore.QObject):
 
         self.plotcanvas.clear()
 
+        # tcl needs to be reinitialized, otherwise  old shell variables etc  remains
+        self.init_tcl()
+
         self.collection.delete_all()
 
         self.setup_component_editor()
@@ -1744,6 +1840,7 @@ class App(QtCore.QObject):
         :param outname:
         :return:
         """
+
         self.log.debug("export_svg()")
 
         try:
@@ -1770,7 +1867,7 @@ class App(QtCore.QObject):
             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 += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">'
             svg_header += '<g transform="scale(1,-1)">'
             svg_footer = '</g> </svg>'
             svg_elem = svg_header + exported_svg + svg_footer
@@ -2409,85 +2506,96 @@ class App(QtCore.QObject):
             return 'Ok'
 
 
-        def geocutout(name, *args):
-            """
+        def geocutout(name=None, *args):
+            '''
+            TCL shell command - see help section
+
             Subtract gaps from geometry, this will not create new object
 
-            :param name:
-            :param args:
-            :return:
-            """
-            a, kwa = h(*args)
-            types = {'dia': float,
-                     'gapsize': float,
-                     'gaps': str}
+            :param name: name of object
+            :param args: array of arguments
+            :return: "Ok" if completed without errors
+            '''
 
-            # How gaps wil be rendered:
-            # lr    - left + right
-            # tb    - top + bottom
-            # 4     - left + right +top + bottom
-            # 2lr   - 2*left + 2*right
-            # 2tb   - 2*top + 2*bottom
-            # 8     - 2*left + 2*right +2*top + 2*bottom
+            try:
+                a, kwa = h(*args)
+                types = {'dia': float,
+                         'gapsize': float,
+                         'gaps': str}
+
+                # How gaps wil be rendered:
+                # lr    - left + right
+                # tb    - top + bottom
+                # 4     - left + right +top + bottom
+                # 2lr   - 2*left + 2*right
+                # 2tb   - 2*top + 2*bottom
+                # 8     - 2*left + 2*right +2*top + 2*bottom
 
-            for key in kwa:
-                if key not in types:
-                    return 'Unknown parameter: %s' % key
-                kwa[key] = types[key](kwa[key])
+                if name is None:
+                    self.raise_tcl_error('Argument name is missing.')
 
-            try:
-                obj = self.collection.get_by_name(str(name))
-            except:
-                return "Could not retrieve object: %s" % name
+                for key in kwa:
+                    if key not in types:
+                        self.raise_tcl_error('Unknown parameter: %s' % key)
+                    try:
+                        kwa[key] = types[key](kwa[key])
+                    except Exception, e:
+                        self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key])))
 
-            # Get min and max data for each object as we just cut rectangles across X or Y
-            xmin, ymin, xmax, ymax = obj.bounds()
-            px = 0.5 * (xmin + xmax)
-            py = 0.5 * (ymin + ymax)
-            lenghtx = (xmax - xmin)
-            lenghty = (ymax - ymin)
-            gapsize = kwa['gapsize'] + kwa['dia'] / 2
-            
-            if kwa['gaps'] == '8' or kwa['gaps']=='2lr':
-                
-                subtract_rectangle(name, 
-                                   xmin - gapsize, 
-                                   py - gapsize + lenghty / 4, 
-                                   xmax + gapsize, 
-                                   py + gapsize + lenghty / 4)
-                subtract_rectangle(name, 
-                                   xmin-gapsize, 
-                                   py - gapsize - lenghty / 4,
-                                   xmax + gapsize,
-                                   py + gapsize - lenghty / 4)
-                
-            if kwa['gaps'] == '8' or kwa['gaps']=='2tb':
-                subtract_rectangle(name, 
-                                   px - gapsize + lenghtx / 4,
-                                   ymin-gapsize, 
-                                   px + gapsize + lenghtx / 4, 
-                                   ymax + gapsize)
-                subtract_rectangle(name,
-                                   px - gapsize - lenghtx / 4, 
-                                   ymin - gapsize,
-                                   px + gapsize - lenghtx / 4,
-                                   ymax + gapsize)
-                
-            if kwa['gaps'] == '4' or kwa['gaps']=='lr':
-                subtract_rectangle(name,
-                                   xmin - gapsize,
-                                   py - gapsize,
-                                   xmax + gapsize,
-                                   py + gapsize)
-                
-            if kwa['gaps'] == '4' or kwa['gaps']=='tb':
-                subtract_rectangle(name,
-                                   px - gapsize,
-                                   ymin - gapsize,
-                                   px + gapsize,
-                                   ymax + gapsize)
-                
-            return 'Ok'
+                try:
+                    obj = self.collection.get_by_name(str(name))
+                except:
+                    self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+                # Get min and max data for each object as we just cut rectangles across X or Y
+                xmin, ymin, xmax, ymax = obj.bounds()
+                px = 0.5 * (xmin + xmax)
+                py = 0.5 * (ymin + ymax)
+                lenghtx = (xmax - xmin)
+                lenghty = (ymax - ymin)
+                gapsize = kwa['gapsize'] + kwa['dia'] / 2
+
+                if kwa['gaps'] == '8' or kwa['gaps']=='2lr':
+
+                    subtract_rectangle(name,
+                                       xmin - gapsize,
+                                       py - gapsize + lenghty / 4,
+                                       xmax + gapsize,
+                                       py + gapsize + lenghty / 4)
+                    subtract_rectangle(name,
+                                       xmin-gapsize,
+                                       py - gapsize - lenghty / 4,
+                                       xmax + gapsize,
+                                       py + gapsize - lenghty / 4)
+
+                if kwa['gaps'] == '8' or kwa['gaps']=='2tb':
+                    subtract_rectangle(name,
+                                       px - gapsize + lenghtx / 4,
+                                       ymin-gapsize,
+                                       px + gapsize + lenghtx / 4,
+                                       ymax + gapsize)
+                    subtract_rectangle(name,
+                                       px - gapsize - lenghtx / 4,
+                                       ymin - gapsize,
+                                       px + gapsize - lenghtx / 4,
+                                       ymax + gapsize)
+
+                if kwa['gaps'] == '4' or kwa['gaps']=='lr':
+                    subtract_rectangle(name,
+                                       xmin - gapsize,
+                                       py - gapsize,
+                                       xmax + gapsize,
+                                       py + gapsize)
+
+                if kwa['gaps'] == '4' or kwa['gaps']=='tb':
+                    subtract_rectangle(name,
+                                       px - gapsize,
+                                       ymin - gapsize,
+                                       px + gapsize,
+                                       ymax + gapsize)
+
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
         def mirror(name, *args):
             a, kwa = h(*args)
@@ -2761,59 +2869,63 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :return: "Ok" if completed without errors
             '''
-            a, kwa = h(*args)
-            types = {'tools': str,
-                     'outname': str,
-                     'drillz': float,
-                     'travelz': float,
-                     'feedrate': float,
-                     'spindlespeed': int,
-                     'toolchange': int
-                     }
 
-            if name is None:
-                self.raiseTclError('Argument name is missing.')
+            try:
+                a, kwa = h(*args)
+                types = {'tools': str,
+                         'outname': str,
+                         'drillz': float,
+                         'travelz': float,
+                         'feedrate': float,
+                         'spindlespeed': int,
+                         'toolchange': int
+                         }
+
+                if name is None:
+                    self.raise_tcl_error('Argument name is missing.')
+
+                for key in kwa:
+                    if key not in types:
+                        self.raise_tcl_error('Unknown parameter: %s' % key)
+                    try:
+                        kwa[key] = types[key](kwa[key])
+                    except Exception, e:
+                        self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key])))
 
-            for key in kwa:
-                if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
                 try:
-                    kwa[key] = types[key](kwa[key])
-                except Exception, e:
-                    self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key])))
+                    obj = self.collection.get_by_name(str(name))
+                except:
+                    self.raise_tcl_error("Could not retrieve object: %s" % name)
 
-            try:
-                obj = self.collection.get_by_name(str(name))
-            except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                if obj is None:
+                    self.raise_tcl_error('Object not found: %s' % name)
 
-            if obj is None:
-                self.raiseTclError('Object not found: %s' % name)
+                if not isinstance(obj, FlatCAMExcellon):
+                    self.raise_tcl_error('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj)))
 
-            if not isinstance(obj, FlatCAMExcellon):
-                self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name,  type(obj)))
+                try:
+                    # Get the tools from the list
+                    job_name = kwa["outname"]
+
+                    # Object initialization function for app.new_object()
+                    def job_init(job_obj, app_obj):
+                        job_obj.z_cut = kwa["drillz"]
+                        job_obj.z_move = kwa["travelz"]
+                        job_obj.feedrate = kwa["feedrate"]
+                        job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None
+                        toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False
+                        job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange)
+                        job_obj.gcode_parse()
+                        job_obj.create_geometry()
+
+                    obj.app.new_object("cncjob", job_name, job_init)
 
-            try:
-                # Get the tools from the list
-                job_name = kwa["outname"]
-
-                # Object initialization function for app.new_object()
-                def job_init(job_obj, app_obj):
-                    job_obj.z_cut = kwa["drillz"]
-                    job_obj.z_move = kwa["travelz"]
-                    job_obj.feedrate = kwa["feedrate"]
-                    job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None
-                    toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False
-                    job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange)
-                    job_obj.gcode_parse()
-                    job_obj.create_geometry()
-
-                obj.app.new_object("cncjob", job_name, job_init)
+                except Exception, e:
+                    self.raise_tcl_error("Operation failed: %s" % str(e))
 
-            except Exception, e:
-                self.raiseTclError("Operation failed: %s" % str(e))
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
-            return 'Ok'
 
         def millholes(name=None, *args):
             '''
@@ -2822,48 +2934,51 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :return: "Ok" if completed without errors
             '''
-            a, kwa = h(*args)
-            types = {'tooldia': float,
-                     'tools': str,
-                     'outname': str}
 
-            if name is None:
-                self.raiseTclError('Argument name is missing.')
+            try:
+                a, kwa = h(*args)
+                types = {'tooldia': float,
+                         'tools': str,
+                         'outname': str}
 
-            for key in kwa:
-                if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
-                try:
-                    kwa[key] = types[key](kwa[key])
-                except Exception, e:
-                    self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key]))
+                if name is None:
+                    self.raise_tcl_error('Argument name is missing.')
 
-            try:
-                if 'tools' in kwa:
-                    kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")]
-            except Exception as e:
-                self.raiseTclError("Bad tools: %s" % str(e))
+                for key in kwa:
+                    if key not in types:
+                        self.raise_tcl_error('Unknown parameter: %s' % key)
+                    try:
+                        kwa[key] = types[key](kwa[key])
+                    except Exception, e:
+                        self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
 
-            try:
-                obj = self.collection.get_by_name(str(name))
-            except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                try:
+                    if 'tools' in kwa:
+                        kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")]
+                except Exception as e:
+                    self.raise_tcl_error("Bad tools: %s" % str(e))
 
-            if obj is None:
-                self.raiseTclError("Object not found: %s" % name)
+                try:
+                    obj = self.collection.get_by_name(str(name))
+                except:
+                    self.raise_tcl_error("Could not retrieve object: %s" % name)
 
-            if not isinstance(obj, FlatCAMExcellon):
-                self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name,  type(obj)))
+                if obj is None:
+                    self.raise_tcl_error("Object not found: %s" % name)
 
-            try:
-                success, msg = obj.generate_milling(**kwa)
-            except Exception as e:
-                self.raiseTclError("Operation failed: %s" % str(e))
+                if not isinstance(obj, FlatCAMExcellon):
+                    self.raise_tcl_error('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj)))
 
-            if not success:
-                self.raiseTclError(msg)
+                try:
+                    success, msg = obj.generate_milling(**kwa)
+                except Exception as e:
+                    self.raise_tcl_error("Operation failed: %s" % str(e))
 
-            return 'Ok'
+                if not success:
+                    self.raise_tcl_error(msg)
+
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
         def exteriors(name=None, *args):
             '''
@@ -2872,46 +2987,49 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :return: "Ok" if completed without errors
             '''
-            a, kwa = h(*args)
-            types = {'outname': str}
 
-            if name is None:
-                self.raiseTclError('Argument name is missing.')
+            try:
+                a, kwa = h(*args)
+                types = {'outname': str}
 
-            for key in kwa:
-                if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
-                try:
-                    kwa[key] = types[key](kwa[key])
-                except Exception, e:
-                    self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key]))
+                if name is None:
+                    self.raise_tcl_error('Argument name is missing.')
 
-            try:
-                obj = self.collection.get_by_name(str(name))
-            except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                for key in kwa:
+                    if key not in types:
+                        self.raise_tcl_error('Unknown parameter: %s' % key)
+                    try:
+                        kwa[key] = types[key](kwa[key])
+                    except Exception, e:
+                        self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
 
-            if obj is None:
-                self.raiseTclError("Object not found: %s" % name)
+                try:
+                    obj = self.collection.get_by_name(str(name))
+                except:
+                    self.raise_tcl_error("Could not retrieve object: %s" % name)
 
-            if not isinstance(obj, Geometry):
-                self.raiseTclError('Expected Geometry, got %s %s.' % (name,  type(obj)))
+                if obj is None:
+                    self.raise_tcl_error("Object not found: %s" % name)
 
-            def geo_init(geo_obj, app_obj):
-                geo_obj.solid_geometry = obj_exteriors
+                if not isinstance(obj, Geometry):
+                    self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
 
-            if 'outname' in kwa:
-                outname = kwa['outname']
-            else:
-                outname = name + ".exteriors"
+                def geo_init(geo_obj, app_obj):
+                    geo_obj.solid_geometry = obj_exteriors
 
-            try:
-                obj_exteriors = obj.get_exteriors()
-                self.new_object('geometry', outname, geo_init)
-            except Exception as e:
-                self.raiseTclError("Failed: %s" % str(e))
+                if 'outname' in kwa:
+                    outname = kwa['outname']
+                else:
+                    outname = name + ".exteriors"
 
-            return 'Ok'
+                try:
+                    obj_exteriors = obj.get_exteriors()
+                    self.new_object('geometry', outname, geo_init)
+                except Exception as e:
+                    self.raise_tcl_error("Failed: %s" % str(e))
+
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
         def interiors(name=None, *args):
             '''
@@ -2920,46 +3038,49 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :return: "Ok" if completed without errors
             '''
-            a, kwa = h(*args)
-            types = {'outname': str}
 
-            for key in kwa:
-                if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
-                try:
-                    kwa[key] = types[key](kwa[key])
-                except Exception, e:
-                    self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key]))
+            try:
+                a, kwa = h(*args)
+                types = {'outname': str}
 
-            if name is None:
-                self.raiseTclError('Argument name is missing.')
+                for key in kwa:
+                    if key not in types:
+                        self.raise_tcl_error('Unknown parameter: %s' % key)
+                    try:
+                        kwa[key] = types[key](kwa[key])
+                    except Exception, e:
+                        self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
 
-            try:
-                obj = self.collection.get_by_name(str(name))
-            except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                if name is None:
+                    self.raise_tcl_error('Argument name is missing.')
 
-            if obj is None:
-                self.raiseTclError("Object not found: %s" % name)
+                try:
+                    obj = self.collection.get_by_name(str(name))
+                except:
+                    self.raise_tcl_error("Could not retrieve object: %s" % name)
 
-            if not isinstance(obj, Geometry):
-                self.raiseTclError('Expected Geometry, got %s %s.' % (name,  type(obj)))
+                if obj is None:
+                    self.raise_tcl_error("Object not found: %s" % name)
 
-            def geo_init(geo_obj, app_obj):
-                geo_obj.solid_geometry = obj_interiors
+                if not isinstance(obj, Geometry):
+                    self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
 
-            if 'outname' in kwa:
-                outname = kwa['outname']
-            else:
-                outname = name + ".interiors"
+                def geo_init(geo_obj, app_obj):
+                    geo_obj.solid_geometry = obj_interiors
 
-            try:
-                obj_interiors = obj.get_interiors()
-                self.new_object('geometry', outname, geo_init)
-            except Exception as e:
-                self.raiseTclError("Failed: %s" % str(e))
+                if 'outname' in kwa:
+                    outname = kwa['outname']
+                else:
+                    outname = name + ".interiors"
 
-            return 'Ok'
+                try:
+                    obj_interiors = obj.get_interiors()
+                    self.new_object('geometry', outname, geo_init)
+                except Exception as e:
+                    self.raise_tcl_error("Failed: %s" % str(e))
+
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
         def isolate(name=None, *args):
             '''
@@ -2977,29 +3098,29 @@ class App(QtCore.QObject):
 
             for key in kwa:
                 if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
+                    self.raise_tcl_error('Unknown parameter: %s' % key)
                 try:
                     kwa[key] = types[key](kwa[key])
                 except Exception, e:
-                    self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key]))
+                    self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
             try:
                 obj = self.collection.get_by_name(str(name))
             except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                self.raise_tcl_error("Could not retrieve object: %s" % name)
 
             if obj is None:
-                self.raiseTclError("Object not found: %s" % name)
+                self.raise_tcl_error("Object not found: %s" % name)
 
             assert isinstance(obj, FlatCAMGerber), \
                 "Expected a FlatCAMGerber, got %s" % type(obj)
 
             if not isinstance(obj, FlatCAMGerber):
-                self.raiseTclError('Expected FlatCAMGerber, got %s %s.' % (name,  type(obj)))
+                self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj)))
 
             try:
                 obj.isolate(**kwa)
             except Exception, e:
-                self.raiseTclError("Operation failed: %s" % str(e))
+                self.raise_tcl_error("Operation failed: %s" % str(e))
 
             return 'Ok'
 
@@ -3390,11 +3511,11 @@ class App(QtCore.QObject):
             Test it like this:
             if name is None:
 
-                self.raiseTclError('Argument name is missing.')
+                self.raise_tcl_error('Argument name is missing.')
 
-            When error ocurre, always use raiseTclError, never return "sometext" on error,
+            When error ocurre, always use raise_tcl_error, never return "sometext" on error,
             otherwise we will miss it and processing will silently continue.
-            Method raiseTclError  pass error into TCL interpreter, then raise python exception,
+            Method raise_tcl_error  pass error into TCL interpreter, then raise python exception,
             which is catched in exec_command and displayed in TCL shell console with red background.
             Error in console is displayed  with TCL  trace.
 
@@ -3776,6 +3897,9 @@ class App(QtCore.QObject):
             }
         }
 
+        #import/overwrite tcl commands as objects of TclCommand descendants
+        tclCommands.register_all_commands(self, commands)
+
         # Add commands to the tcl interpreter
         for cmd in commands:
             self.tcl.createcommand(cmd, commands[cmd]['fcn'])

+ 20 - 11
FlatCAMObj.py

@@ -1040,6 +1040,10 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
         self.app.inform.emit("Saved to: " + filename)
 
+    def get_gcode(self, preamble='', postamble=''):
+        #we need this to beable get_gcode separatelly for shell command export_code
+        return preamble + '\n' + self.gcode + "\n" + postamble
+
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
             return
@@ -1243,7 +1247,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                        outname=None,
                        spindlespeed=None,
                        multidepth=None,
-                       depthperpass=None):
+                       depthperpass=None,
+                       use_thread=True):
         """
         Creates a CNCJob out of this Geometry object. The actual
         work is done by the target FlatCAMCNCjob object's
@@ -1304,18 +1309,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
             app_obj.progress.emit(80)
 
-        # To be run in separate thread
-        def job_thread(app_obj):
-            with self.app.proc_container.new("Generating CNC Job."):
-                app_obj.new_object("cncjob", outname, job_init)
-                app_obj.inform.emit("CNCjob created: %s" % outname)
-                app_obj.progress.emit(100)
 
-        # Create a promise with the name
-        self.app.collection.promise(outname)
+        if  use_thread:
+            # To be run in separate thread
+            def job_thread(app_obj):
+                with self.app.proc_container.new("Generating CNC Job."):
+                    app_obj.new_object("cncjob", outname, job_init)
+                    app_obj.inform.emit("CNCjob created: %s" % outname)
+                    app_obj.progress.emit(100)
 
-        # Send to worker
-        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+            # Create a promise with the name
+            self.app.collection.promise(outname)
+
+            # Send to worker
+            self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+        else:
+            self.app.new_object("cncjob", outname, job_init)
 
     def on_plot_cb_click(self, *args):  # TODO: args not needed
         if self.muted_ui:

+ 23 - 6
FlatCAMWorker.py

@@ -1,5 +1,4 @@
 from PyQt4 import QtCore
-#import FlatCAMApp
 
 
 class Worker(QtCore.QObject):
@@ -8,15 +7,34 @@ class Worker(QtCore.QObject):
     in a single independent thread.
     """
 
+    # avoid multiple tests  for debug availability
+    pydevd_failed = False
+
     def __init__(self, app, name=None):
         super(Worker, self).__init__()
         self.app = app
         self.name = name
 
+    def allow_debug(self):
+        """
+         allow debuging/breakpoints in this threads
+         should work from PyCharm and PyDev
+        :return:
+        """
+
+        if not self.pydevd_failed:
+            try:
+                import pydevd
+                pydevd.settrace(suspend=False, trace_only_current_thread=True)
+            except ImportError:
+                self.pydevd_failed=True
+
     def run(self):
 
         self.app.log.debug("Worker Started!")
 
+        self.allow_debug()
+
         # Tasks are queued in the event listener.
         self.app.worker_task.connect(self.do_worker_task)
 
@@ -24,10 +42,10 @@ class Worker(QtCore.QObject):
 
         self.app.log.debug("Running task: %s" % str(task))
 
-        # 'worker_name' property of task allows to target
-        # specific worker.
+        self.allow_debug()
+
         if ('worker_name' in task and task['worker_name'] == self.name) or \
-                ('worker_name' not in task and self.name is None):
+            ('worker_name' not in task and self.name is None):
 
             try:
                 task['fcn'](*task['params'])
@@ -37,5 +55,4 @@ class Worker(QtCore.QObject):
 
             return
 
-        # FlatCAMApp.App.log.debug("Task ignored.")
-        self.app.log.debug("Task ignored.")
+        self.app.log.debug("Task ignored.")

+ 41 - 18
camlib.py

@@ -136,6 +136,27 @@ class Geometry(object):
             log.error("Failed to run union on polygons.")
             raise
 
+    def add_polyline(self, points):
+        """
+        Adds a polyline to the object (by union)
+
+        :param points: The vertices of the polyline.
+        :return: None
+        """
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            self.solid_geometry.append(LineString(points))
+            return
+
+        try:
+            self.solid_geometry = self.solid_geometry.union(LineString(points))
+        except:
+            #print "Failed to run union on polygons."
+            log.error("Failed to run union on polylines.")
+            raise
+
     def subtract_polygon(self, points):
         """
         Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
@@ -2756,7 +2777,7 @@ class CNCjob(Geometry):
         # so we actually are sorting the tools by diameter
         sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
         if tools == "all":
-            tools = str([i[0] for i in sorted_tools])   # we get a string of ordered tools
+            tools = [i[0] for i in sorted_tools]   # we get a array of ordered tools
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
         else:
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
@@ -2797,24 +2818,26 @@ class CNCjob(Geometry):
 
         for tool in tools:
 
-            # Tool change sequence (optional)
-            if toolchange:
-                gcode += "G00 Z%.4f\n" % toolchangez
-                gcode += "T%d\n" % int(tool)  # Indicate tool slot (for automatic tool changer)
-                gcode += "M5\n"  # Spindle Stop
-                gcode += "M6\n"  # Tool change
-                gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
-                gcode += "M0\n"  # Temporary machine stop
-                if self.spindlespeed is not None:
-                    gcode += "M03 S%d\n" % int(self.spindlespeed)  # Spindle start with configured speed
-                else:
-                    gcode += "M03\n"  # Spindle start
+            # only if tool have some points, otherwise thre may be error and this part is useless
+            if tool in points:
+                # Tool change sequence (optional)
+                if toolchange:
+                    gcode += "G00 Z%.4f\n" % toolchangez
+                    gcode += "T%d\n" % int(tool)  # Indicate tool slot (for automatic tool changer)
+                    gcode += "M5\n"  # Spindle Stop
+                    gcode += "M6\n"  # Tool change
+                    gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
+                    gcode += "M0\n"  # Temporary machine stop
+                    if self.spindlespeed is not None:
+                        gcode += "M03 S%d\n" % int(self.spindlespeed)  # Spindle start with configured speed
+                    else:
+                        gcode += "M03\n"  # Spindle start
 
-            # Drillling!
-            for point in points[tool]:
-                x, y = point.coords.xy
-                gcode += t % (x[0], y[0])
-                gcode += down + up
+                # Drillling!
+                for point in points[tool]:
+                    x, y = point.coords.xy
+                    gcode += t % (x[0], y[0])
+                    gcode += down + up
 
         gcode += t % (0, 0)
         gcode += "M05\n"  # Spindle stop


BIN
descartes/__init__.pyc


BIN
descartes/patch.pyc


+ 394 - 0
tclCommands/TclCommand.py

@@ -0,0 +1,394 @@
+import sys
+import re
+import FlatCAMApp
+import abc
+import collections
+from PyQt4 import QtCore
+from contextlib import contextmanager
+
+
+class TclCommand(object):
+
+    # FlatCAMApp
+    app = None
+
+    # logger
+    log = None
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = []
+
+    # dictionary of types from Tcl command, needs to be ordered
+    # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+    option_types = collections.OrderedDict()
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+    help = {
+        'main': "undefined help.",
+        'args': collections.OrderedDict([
+            ('argumentname', 'undefined help.'),
+            ('optionname', 'undefined help.')
+        ]),
+        'examples': []
+    }
+
+    # original incoming arguments into command
+    original_args = None
+
+    def __init__(self, app):
+        self.app = app
+        if self.app is None:
+            raise TypeError('Expected app to be FlatCAMApp instance.')
+        if not isinstance(self.app, FlatCAMApp.App):
+            raise TypeError('Expected FlatCAMApp, got %s.' % type(app))
+        self.log = self.app.log
+
+    def raise_tcl_error(self, text):
+        """
+        this method  pass exception from python into TCL as error, so we get stacktrace and reason
+        this is  only redirect to self.app.raise_tcl_error
+        :param text: text of error
+        :return: none
+        """
+
+        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):
+        """
+        Decorate help for TCL console output.
+
+        :return: decorated help from structure
+        """
+
+        def get_decorated_command(alias_name):
+            command_string = []
+            for arg_key, arg_type in self.help['args'].items():
+                command_string.append(get_decorated_argument(arg_key, arg_type, True))
+            return "> " + alias_name + " " + " ".join(command_string)
+
+        def get_decorated_argument(help_key, help_text, in_command=False):
+            option_symbol = ''
+            if help_key in self.arg_names:
+                arg_type = self.arg_names[help_key]
+                type_name = str(arg_type.__name__)
+                in_command_name = "<" + type_name + ">"
+            elif help_key in self.option_types:
+                option_symbol = '-'
+                arg_type = self.option_types[help_key]
+                type_name = str(arg_type.__name__)
+                in_command_name = option_symbol + help_key + " <" + type_name + ">"
+            else:
+                option_symbol = ''
+                type_name = '?'
+                in_command_name = option_symbol + help_key + " <" + type_name + ">"
+
+            if in_command:
+                if help_key in self.required:
+                    return in_command_name
+                else:
+                    return '[' + in_command_name + "]"
+            else:
+                if help_key in self.required:
+                    return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text
+                else:
+                    return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]"
+
+        def get_decorated_example(example_item):
+            return "> "+example_item
+
+        help_string = [self.help['main']]
+        for alias in self.aliases:
+            help_string.append(get_decorated_command(alias))
+
+        for key, value in self.help['args'].items():
+            help_string.append(get_decorated_argument(key, value))
+
+        # timeout is unique for signaled commands (this is not best oop practice, but much easier for now)
+        if isinstance(self, TclCommandSignaled):
+            help_string.append("\t[-timeout <int>: Max wait for job timeout before error.]")
+
+        for example in self.help['examples']:
+            help_string.append(get_decorated_example(example))
+
+        return "\n".join(help_string)
+
+    @staticmethod
+    def parse_arguments(args):
+            """
+            Pre-processes arguments to detect '-keyword value' pairs into dictionary
+            and standalone parameters into list.
+
+            This is copy from FlatCAMApp.setup_shell().h() just for accessibility,
+            original should  be removed  after all commands will be converted
+
+            :param args: arguments from tcl to parse
+            :return: arguments, options
+            """
+
+            options = {}
+            arguments = []
+            n = len(args)
+            name = None
+            for i in range(n):
+                match = re.search(r'^-([a-zA-Z].*)', args[i])
+                if match:
+                    assert name is None
+                    name = match.group(1)
+                    continue
+
+                if name is None:
+                    arguments.append(args[i])
+                else:
+                    options[name] = args[i]
+                    name = None
+
+            return arguments, options
+
+    def check_args(self, args):
+        """
+        Check arguments and  options for right types
+
+        :param args: arguments from tcl to check
+        :return: named_args, unnamed_args
+        """
+
+        arguments, options = self.parse_arguments(args)
+
+        named_args = {}
+        unnamed_args = []
+
+        # check arguments
+        idx = 0
+        arg_names_items = self.arg_names.items()
+        for argument in arguments:
+            if len(self.arg_names) > idx:
+                key, arg_type = arg_names_items[idx]
+                try:
+                    named_args[key] = arg_type(argument)
+                except Exception, e:
+                    self.raise_tcl_error("Cannot cast named argument '%s' to type %s  with exception '%s'."
+                                         % (key, arg_type, str(e)))
+            else:
+                unnamed_args.append(argument)
+            idx += 1
+
+        # check options
+        for key in options:
+            if key not in self.option_types and key != 'timeout':
+                self.raise_tcl_error('Unknown parameter: %s' % key)
+            try:
+                if key != 'timeout':
+                    named_args[key] = self.option_types[key](options[key])
+                else:
+                    named_args[key] = int(options[key])
+            except Exception, e:
+                self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
+                                     % (key, self.option_types[key], str(e)))
+
+        # check required arguments
+        for key in self.required:
+            if key not in named_args:
+                self.raise_tcl_error("Missing required argument '%s'." % key)
+
+        return named_args, unnamed_args
+
+
+    def raise_tcl_unknown_error(self, unknownException):
+        """
+        raise Exception if is different type  than TclErrorException
+        this is here mainly to show unknown errors inside TCL shell console
+        :param unknownException:
+        :return:
+        """
+
+        #if not isinstance(unknownException, self.TclErrorException):
+        #    self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+        #else:
+        raise unknownException
+
+    def raise_tcl_error(self, text):
+        """
+        this method  pass exception from python into TCL as error, so we get stacktrace and reason
+        :param text: text of error
+        :return: raise exception
+        """
+
+        # becouse of signaling we cannot call error to TCL from here but when task is finished
+        # also nonsiglaned arwe handled here to better exception handling and  diplay after   command is finished
+        raise self.app.TclErrorException(text)
+
+    def execute_wrapper(self, *args):
+        """
+        Command which is called by tcl console when current commands aliases are hit.
+        Main catch(except) is implemented here.
+        This method should be reimplemented only when initial checking sequence differs
+
+        :param args: arguments passed from tcl command console
+        :return: None, output text or exception
+        """
+
+        #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
+
+        try:
+            self.log.debug("TCL command '%s' executed." % str(self.__class__))
+            self.original_args=args
+            args, unnamed_args = self.check_args(args)
+            return self.execute(args, unnamed_args)
+        except Exception as unknown:
+            error_info=sys.exc_info()
+            self.log.error("TCL command '%s' failed." % str(self))
+            self.app.display_tcl_error(unknown, error_info)
+            self.raise_tcl_unknown_error(unknown)
+
+    @abc.abstractmethod
+    def execute(self, args, unnamed_args):
+        """
+        Direct execute of command, this method should be implemented in each descendant.
+        No main catch should be implemented here.
+
+        :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, output text or exception
+        """
+
+        raise NotImplementedError("Please Implement this method")
+
+class TclCommandSignaled(TclCommand):
+    """
+        !!! I left it here only  for demonstration !!!
+        Go to TclCommandCncjob and  into class definition put
+            class TclCommandCncjob(TclCommand.TclCommandSignaled):
+        also change
+            obj.generatecncjob(use_thread = False, **args)
+        to
+            obj.generatecncjob(use_thread = True, **args)
+
+
+        This class is  child of  TclCommand and is used for commands  which create  new objects
+        it handles  all neccessary stuff about blocking and passing exeptions
+    """
+
+    output = None
+
+    def execute_call(self, args, unnamed_args):
+
+        try:
+            self.output = None
+            self.error=None
+            self.error_info=None
+            self.output = self.execute(args, unnamed_args)
+        except Exception as unknown:
+            self.error_info = sys.exc_info()
+            self.error=unknown
+        finally:
+            self.app.shell_command_finished.emit(self)
+
+    def execute_wrapper(self, *args):
+        """
+        Command which is called by tcl console when current commands aliases are hit.
+        Main catch(except) is implemented here.
+        This method should be reimplemented only when initial checking sequence differs
+
+        :param args: arguments passed from tcl command console
+        :return: None, output text or exception
+        """
+
+        @contextmanager
+        def wait_signal(signal, timeout=300000):
+            """Block loop until signal emitted, or timeout (ms) elapses."""
+            loop = QtCore.QEventLoop()
+
+            # Normal termination
+            signal.connect(loop.quit)
+
+            # Termination by exception in thread
+            self.app.thread_exception.connect(loop.quit)
+
+            status = {'timed_out': False}
+
+            def report_quit():
+                status['timed_out'] = True
+                loop.quit()
+
+            yield
+
+            # Temporarily change how exceptions are managed.
+            oeh = sys.excepthook
+            ex = []
+
+            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:
+                raise ex[0]
+
+            if status['timed_out']:
+                self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout <miliseconds>' for command or 'set_sys background_timeout <miliseconds>'.")
+
+        try:
+            self.log.debug("TCL command '%s' executed." % str(self.__class__))
+            self.original_args=args
+            args, unnamed_args = self.check_args(args)
+            if 'timeout' in args:
+                passed_timeout=args['timeout']
+                del args['timeout']
+            else:
+                passed_timeout= self.app.defaults['background_timeout']
+
+            # set detail for processing, it will be there until next open or close
+            self.app.shell.open_proccessing(self.get_current_command())
+
+            def handle_finished(obj):
+                self.app.shell_command_finished.disconnect(handle_finished)
+                if self.error is not None:
+                    self.raise_tcl_unknown_error(self.error)
+
+            self.app.shell_command_finished.connect(handle_finished)
+
+            with wait_signal(self.app.shell_command_finished, passed_timeout):
+                # every TclCommandNewObject ancestor  support  timeout as parameter,
+                # but it does not mean anything for child itself
+                # when operation  will be  really long is good  to set it higher then defqault 30s
+                self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
+
+            return self.output
+
+        except Exception as unknown:
+            error_info=sys.exc_info()
+            self.log.error("TCL command '%s' failed." % str(self))
+            self.app.display_tcl_error(unknown, error_info)
+            self.raise_tcl_unknown_error(unknown)

+ 61 - 0
tclCommands/TclCommandAddPolygon.py

@@ -0,0 +1,61 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandAddPolygon(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to create a polygon in the given Geometry object
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['add_polygon', 'add_poly']
+
+    # 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()
+
+    # 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 a polygon in the given Geometry object.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the Geometry object to which to append the polygon.'),
+            ('xi, yi', 'Coordinates of points in the polygon.')
+        ]),
+        'examples': [
+            'add_polygon <name> <x0> <y0> <x1> <y1> <x2> <y2> [x3 y3 [...]]'
+        ]
+    }
+
+    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']
+
+        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, Geometry):
+            self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+        if len(unnamed_args) % 2 != 0:
+            self.raise_tcl_error("Incomplete coordinates.")
+
+        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+
+        obj.add_polygon(points)
+        obj.plot()

+ 61 - 0
tclCommands/TclCommandAddPolyline.py

@@ -0,0 +1,61 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandAddPolyline(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to create a polyline in the given Geometry object
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['add_polyline']
+
+    # 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()
+
+    # 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 a polyline in the given Geometry object.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the Geometry object to which to append the polyline.'),
+            ('xi, yi', 'Coordinates of points in the polyline.')
+        ]),
+        'examples': [
+            'add_polyline <name> <x0> <y0> <x1> <y1> <x2> <y2> [x3 y3 [...]]'
+        ]
+    }
+
+    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']
+
+        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, Geometry):
+            self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+        if len(unnamed_args) % 2 != 0:
+            self.raise_tcl_error("Incomplete coordinates.")
+
+        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+
+        obj.add_polyline(points)
+        obj.plot()

+ 80 - 0
tclCommands/TclCommandCncjob.py

@@ -0,0 +1,80 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandCncjob(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to Generates a CNC Job from a Geometry Object.
+
+    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 = ['cncjob']
+
+    # 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([
+        ('z_cut',float),
+        ('z_move',float),
+        ('feedrate',float),
+        ('tooldia',float),
+        ('spindlespeed',int),
+        ('multidepth',bool),
+        ('depthperpass',float),
+        ('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': "Generates a CNC Job from a Geometry Object.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the source object.'),
+            ('z_cut', 'Z-axis cutting position.'),
+            ('z_move', 'Z-axis moving position.'),
+            ('feedrate', 'Moving speed when cutting.'),
+            ('tooldia', 'Tool diameter to show on screen.'),
+            ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
+            ('multidepth', 'Use or not multidepth cnccut.'),
+            ('depthperpass', 'Height of one layer for multidepth.'),
+            ('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 + "_cnc"
+
+        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, FlatCAMGeometry):
+            self.raise_tcl_error('Expected FlatCAMGeometry, got %s %s.' % (name, type(obj)))
+
+        del args['name']
+        obj.generatecncjob(use_thread = False, **args)

+ 81 - 0
tclCommands/TclCommandDrillcncjob.py

@@ -0,0 +1,81 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandDrillcncjob(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to Generates a Drill CNC Job from a Excellon Object.
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['drillcncjob']
+
+    # 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([
+        ('tools',str),
+        ('drillz',float),
+        ('travelz',float),
+        ('feedrate',float),
+        ('spindlespeed',int),
+        ('toolchange',bool),
+        ('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': "Generates a Drill CNC Job from a Excellon Object.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the source object.'),
+            ('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'),
+            ('drillz', 'Drill depth into material (example: -2.0).'),
+            ('travelz', 'Travel distance above material (example: 2.0).'),
+            ('feedrate', 'Drilling feed rate.'),
+            ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
+            ('toolchange', 'Enable tool changes (example: True).'),
+            ('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 + "_cnc"
+
+        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, FlatCAMExcellon):
+            self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj)))
+
+        def job_init(job_obj, app):
+            job_obj.z_cut = args["drillz"]
+            job_obj.z_move = args["travelz"]
+            job_obj.feedrate = args["feedrate"]
+            job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
+            toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
+            tools = args["tools"] if "tools" in args else 'all'
+            job_obj.generate_from_excellon_by_tool(obj, tools, toolchange)
+            job_obj.gcode_parse()
+            job_obj.create_geometry()
+
+        self.app.new_object("cncjob", args['outname'], job_init)

+ 79 - 0
tclCommands/TclCommandExportGcode.py

@@ -0,0 +1,79 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandExportGcode(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to export gcode as  tcl output for "set X [export_gcode ...]"
+
+    Requires name to be available. It might still be in the
+    making at the time this function is called, so check for
+    promises and send to background if there are promises.
+
+
+    this  export   may be  catched   by tcl and past as preable  to another  export_gcode or write_gcode
+    this can be used to join GCODES
+
+    example:
+        set_sys units MM
+        new
+        open_gerber tests/gerber_files/simple1.gbr -outname margin
+        isolate margin -dia 3
+        cncjob margin_iso
+        cncjob margin_iso
+        set EXPORT [export_gcode margin_iso_cnc]
+        write_gcode margin_iso_cnc_1 /tmp/file.gcode ${EXPORT}
+
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['export_gcode']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str),
+        ('preamble', str),
+        ('postamble', str)
+    ])
+
+    # 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 = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Export gcode into console output.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the source Geometry object.'),
+            ('preamble', 'Prepend GCODE.'),
+            ('postamble', 'Append GCODE.')
+        ]),
+        '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']
+
+        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, CNCjob):
+            self.raise_tcl_error('Expected CNCjob, got %s %s.' % (name, type(obj)))
+
+        if self.app.collection.has_promises():
+            self.raise_tcl_error('!!!Promises exists, but should not here!!!')
+
+        del args['name']
+        return obj.get_gcode(**args)

+ 64 - 0
tclCommands/TclCommandExteriors.py

@@ -0,0 +1,64 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandExteriors(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to get exteriors of polygons
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['exteriors', 'ext']
+
+    # 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([
+        ('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': "Get exteriors of polygons.",
+        'args':  collections.OrderedDict([
+            ('name', 'Name of the source Geometry object.'),
+            ('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' in args:
+            outname = args['outname']
+        else:
+            outname = name + "_exteriors"
+
+        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, Geometry):
+            self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+        def geo_init(geo_obj, app_obj):
+            geo_obj.solid_geometry = obj_exteriors
+
+        obj_exteriors = obj.get_exteriors()
+        self.app.new_object('geometry', outname, geo_init)

+ 64 - 0
tclCommands/TclCommandInteriors.py

@@ -0,0 +1,64 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandInteriors(TclCommand.TclCommandSignaled):
+    """
+    Tcl shell command to get interiors of polygons
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['interiors']
+
+    # 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([
+        ('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': "Get interiors of polygons.",
+        'args':  collections.OrderedDict([
+            ('name', 'Name of the source Geometry object.'),
+            ('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' in args:
+            outname = args['outname']
+        else:
+            outname = name + "_interiors"
+
+        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, Geometry):
+            self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+        def geo_init(geo_obj, app_obj):
+            geo_obj.solid_geometry = obj_exteriors
+
+        obj_exteriors = obj.get_interiors()
+        self.app.new_object('geometry', outname, geo_init)

+ 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)

+ 52 - 0
tclCommands/__init__.py

@@ -0,0 +1,52 @@
+import pkgutil
+import sys
+
+# allowed command modules (please append them alphabetically ordered)
+import tclCommands.TclCommandAddPolygon
+import tclCommands.TclCommandAddPolyline
+import tclCommands.TclCommandCncjob
+import tclCommands.TclCommandDrillcncjob
+import tclCommands.TclCommandExportGcode
+import tclCommands.TclCommandExteriors
+import tclCommands.TclCommandInteriors
+import tclCommands.TclCommandIsolate
+import tclCommands.TclCommandNew
+import tclCommands.TclCommandOpenGerber
+
+
+__all__ = []
+
+for loader, name, is_pkg in pkgutil.walk_packages(__path__):
+    module = loader.find_module(name).load_module(name)
+    __all__.append(name)
+
+def register_all_commands(app, commands):
+    """
+    Static method which register all known commands.
+
+    Command should  be for now in directory tclCommands and module should start with TCLCommand
+    Class  have to follow same  name as module.
+
+    we need import all  modules  in top section:
+    import tclCommands.TclCommandExteriors
+    at this stage we can include only wanted  commands  with this, auto loading may be implemented in future
+    I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc.
+
+    :param app: FlatCAMApp
+    :param commands: array of commands  which should be modified
+    :return: None
+    """
+
+    tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')}
+
+    for key, mod in tcl_modules.items():
+        if key != 'tclCommands.TclCommand':
+            class_name = key.split('.')[1]
+            class_type = getattr(mod, class_name)
+            command_instance = class_type(app)
+
+            for alias in command_instance.aliases:
+                commands[alias] = {
+                    'fcn': command_instance.execute_wrapper,
+                    'help': command_instance.get_decorated_help()
+                }

+ 29 - 4
termwidget.py

@@ -4,8 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history.
 """
 
 import cgi
-
-from PyQt4.QtCore import pyqtSignal
+from PyQt4.QtCore import pyqtSignal, Qt
 from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
                         QSizePolicy, QTextCursor, QTextEdit, \
                         QVBoxLayout, QWidget
@@ -83,7 +82,6 @@ class _ExpandableTextEdit(QTextEdit):
         # Paste only plain text.
         self.insertPlainText(mime_data.text())
 
-
 class TermWidget(QWidget):
     """
     Widget wich represents terminal. It only displays text and allows to enter text.
@@ -118,6 +116,34 @@ class TermWidget(QWidget):
 
         self._edit.setFocus()
 
+    def open_proccessing(self, detail=None):
+        """
+        Open processing and disable using shell commands  again until all commands are finished
+
+        :param detail: text detail about what is currently called from TCL to python
+        :return: None
+        """
+
+        self._edit.setTextColor(Qt.white)
+        self._edit.setTextBackgroundColor(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(Qt.black)
+        self._edit.setTextBackgroundColor(Qt.white)
+        self._edit.setPlainText('')
+        self._edit.setDisabled(False)
+
     def _append_to_browser(self, style, text):
         """
         Convert text to HTML for inserting it to browser
@@ -225,4 +251,3 @@ class TermWidget(QWidget):
             self._historyIndex -= 1
             self._edit.setPlainText(self._history[self._historyIndex])
             self._edit.moveCursor(QTextCursor.End)
-

+ 26 - 0
tests/gerber_files/detector_contour.gbr

@@ -0,0 +1,26 @@
+G04 MADE WITH FRITZING*
+G04 WWW.FRITZING.ORG*
+G04 DOUBLE SIDED*
+G04 HOLES PLATED*
+G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
+%ASAXBY*%
+%FSLAX23Y23*%
+%MOIN*%
+%OFA0B0*%
+%SFA1.0B1.0*%
+%ADD10R,1.771650X1.181100*%
+%ADD11C,0.008000*%
+%ADD10C,0.008*%
+%LNCONTOUR*%
+G90*
+G70*
+G54D10*
+G54D11*
+X4Y1177D02*
+X1768Y1177D01*
+X1768Y4D01*
+X4Y4D01*
+X4Y1177D01*
+D02*
+G04 End of contour*
+M02*

+ 2146 - 0
tests/gerber_files/detector_copper_bottom.gbr

@@ -0,0 +1,2146 @@
+G04 MADE WITH FRITZING*
+G04 WWW.FRITZING.ORG*
+G04 DOUBLE SIDED*
+G04 HOLES PLATED*
+G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
+%ASAXBY*%
+%FSLAX23Y23*%
+%MOIN*%
+%OFA0B0*%
+%SFA1.0B1.0*%
+%ADD10C,0.075000*%
+%ADD11C,0.099055*%
+%ADD12C,0.078740*%
+%ADD13R,0.075000X0.075000*%
+%ADD14C,0.048000*%
+%ADD15C,0.020000*%
+%ADD16R,0.001000X0.001000*%
+%LNCOPPER0*%
+G90*
+G70*
+G54D10*
+X1149Y872D03*
+X1349Y872D03*
+X749Y722D03*
+X749Y522D03*
+X1149Y522D03*
+X1449Y522D03*
+X1149Y422D03*
+X1449Y422D03*
+X1149Y322D03*
+X1449Y322D03*
+X1149Y222D03*
+X1449Y222D03*
+X949Y472D03*
+X949Y72D03*
+G54D11*
+X749Y972D03*
+X599Y972D03*
+X349Y322D03*
+X349Y472D03*
+X349Y672D03*
+X349Y822D03*
+G54D10*
+X699Y122D03*
+X699Y322D03*
+G54D12*
+X699Y222D03*
+X949Y972D03*
+X749Y622D03*
+X1049Y222D03*
+X1249Y872D03*
+G54D13*
+X1149Y872D03*
+X1149Y522D03*
+G54D14*
+X949Y373D02*
+X949Y433D01*
+D02*
+X999Y323D02*
+X949Y373D01*
+D02*
+X1109Y322D02*
+X999Y323D01*
+D02*
+X499Y873D02*
+X1109Y872D01*
+D02*
+X1299Y73D02*
+X989Y72D01*
+D02*
+X1399Y322D02*
+X1349Y272D01*
+D02*
+X1349Y272D02*
+X1349Y122D01*
+D02*
+X1349Y122D02*
+X1299Y73D01*
+D02*
+X1409Y322D02*
+X1399Y322D01*
+D02*
+X909Y72D02*
+X749Y73D01*
+D02*
+X749Y73D02*
+X727Y94D01*
+D02*
+X649Y522D02*
+X709Y522D01*
+D02*
+X599Y473D02*
+X649Y522D01*
+D02*
+X401Y472D02*
+X599Y473D01*
+D02*
+X789Y522D02*
+X899Y522D01*
+D02*
+X709Y722D02*
+X599Y722D01*
+D02*
+X599Y722D02*
+X549Y673D01*
+D02*
+X549Y673D02*
+X401Y672D01*
+D02*
+X1149Y562D02*
+X1149Y833D01*
+D02*
+X499Y972D02*
+X499Y873D01*
+D02*
+X547Y972D02*
+X499Y972D01*
+D02*
+X699Y283D02*
+X699Y260D01*
+D02*
+X749Y562D02*
+X749Y584D01*
+D02*
+X499Y873D02*
+X499Y972D01*
+D02*
+X499Y972D02*
+X547Y972D01*
+D02*
+X401Y823D02*
+X449Y823D01*
+D02*
+X899Y522D02*
+X921Y500D01*
+D02*
+X1309Y872D02*
+X1287Y872D01*
+D02*
+X449Y823D02*
+X499Y873D01*
+D02*
+X1349Y422D02*
+X1349Y833D01*
+D02*
+X1189Y422D02*
+X1349Y422D01*
+D02*
+X1399Y322D02*
+X1409Y322D01*
+D02*
+X1349Y372D02*
+X1399Y322D01*
+D02*
+X1349Y422D02*
+X1349Y372D01*
+D02*
+X1189Y422D02*
+X1349Y422D01*
+D02*
+X801Y972D02*
+X911Y972D01*
+D02*
+X1109Y222D02*
+X1087Y222D01*
+D02*
+X401Y322D02*
+X659Y322D01*
+D02*
+X1399Y972D02*
+X987Y972D01*
+D02*
+X1449Y923D02*
+X1399Y972D01*
+D02*
+X1449Y562D02*
+X1449Y923D01*
+G54D15*
+X776Y695D02*
+X721Y695D01*
+X721Y750D01*
+X776Y750D01*
+X776Y695D01*
+D02*
+X671Y150D02*
+X726Y150D01*
+X726Y95D01*
+X671Y95D01*
+X671Y150D01*
+D02*
+G54D16*
+X766Y1112D02*
+X769Y1112D01*
+X764Y1111D02*
+X771Y1111D01*
+X763Y1110D02*
+X772Y1110D01*
+X762Y1109D02*
+X772Y1109D01*
+X762Y1108D02*
+X773Y1108D01*
+X762Y1107D02*
+X773Y1107D01*
+X762Y1106D02*
+X773Y1106D01*
+X762Y1105D02*
+X773Y1105D01*
+X762Y1104D02*
+X773Y1104D01*
+X762Y1103D02*
+X773Y1103D01*
+X762Y1102D02*
+X773Y1102D01*
+X762Y1101D02*
+X773Y1101D01*
+X762Y1100D02*
+X773Y1100D01*
+X762Y1099D02*
+X773Y1099D01*
+X762Y1098D02*
+X773Y1098D01*
+X762Y1097D02*
+X773Y1097D01*
+X762Y1096D02*
+X773Y1096D01*
+X762Y1095D02*
+X773Y1095D01*
+X762Y1094D02*
+X773Y1094D01*
+X762Y1093D02*
+X773Y1093D01*
+X762Y1092D02*
+X773Y1092D01*
+X762Y1091D02*
+X773Y1091D01*
+X762Y1090D02*
+X773Y1090D01*
+X762Y1089D02*
+X773Y1089D01*
+X566Y1088D02*
+X618Y1088D01*
+X741Y1088D02*
+X793Y1088D01*
+X565Y1087D02*
+X620Y1087D01*
+X740Y1087D02*
+X795Y1087D01*
+X564Y1086D02*
+X621Y1086D01*
+X739Y1086D02*
+X796Y1086D01*
+X563Y1085D02*
+X621Y1085D01*
+X738Y1085D02*
+X796Y1085D01*
+X563Y1084D02*
+X622Y1084D01*
+X738Y1084D02*
+X796Y1084D01*
+X563Y1083D02*
+X622Y1083D01*
+X738Y1083D02*
+X796Y1083D01*
+X563Y1082D02*
+X622Y1082D01*
+X738Y1082D02*
+X796Y1082D01*
+X563Y1081D02*
+X622Y1081D01*
+X738Y1081D02*
+X796Y1081D01*
+X563Y1080D02*
+X622Y1080D01*
+X738Y1080D02*
+X796Y1080D01*
+X563Y1079D02*
+X622Y1079D01*
+X739Y1079D02*
+X795Y1079D01*
+X563Y1078D02*
+X622Y1078D01*
+X739Y1078D02*
+X795Y1078D01*
+X563Y1077D02*
+X622Y1077D01*
+X741Y1077D02*
+X794Y1077D01*
+X563Y1076D02*
+X622Y1076D01*
+X762Y1076D02*
+X773Y1076D01*
+X563Y1075D02*
+X621Y1075D01*
+X762Y1075D02*
+X773Y1075D01*
+X563Y1074D02*
+X621Y1074D01*
+X762Y1074D02*
+X773Y1074D01*
+X564Y1073D02*
+X620Y1073D01*
+X762Y1073D02*
+X773Y1073D01*
+X565Y1072D02*
+X619Y1072D01*
+X762Y1072D02*
+X773Y1072D01*
+X569Y1071D02*
+X615Y1071D01*
+X762Y1071D02*
+X773Y1071D01*
+X762Y1070D02*
+X773Y1070D01*
+X762Y1069D02*
+X773Y1069D01*
+X762Y1068D02*
+X773Y1068D01*
+X762Y1067D02*
+X773Y1067D01*
+X762Y1066D02*
+X773Y1066D01*
+X762Y1065D02*
+X773Y1065D01*
+X762Y1064D02*
+X773Y1064D01*
+X762Y1063D02*
+X773Y1063D01*
+X762Y1062D02*
+X773Y1062D01*
+X762Y1061D02*
+X773Y1061D01*
+X762Y1060D02*
+X773Y1060D01*
+X762Y1059D02*
+X773Y1059D01*
+X762Y1058D02*
+X773Y1058D01*
+X762Y1057D02*
+X773Y1057D01*
+X762Y1056D02*
+X773Y1056D01*
+X763Y1055D02*
+X772Y1055D01*
+X763Y1054D02*
+X771Y1054D01*
+X765Y1053D02*
+X770Y1053D01*
+X1661Y878D02*
+X1697Y878D01*
+X1658Y877D02*
+X1698Y877D01*
+X1656Y876D02*
+X1700Y876D01*
+X1653Y875D02*
+X1701Y875D01*
+X1651Y874D02*
+X1701Y874D01*
+X1648Y873D02*
+X1702Y873D01*
+X1645Y872D02*
+X1702Y872D01*
+X1643Y871D02*
+X1702Y871D01*
+X1640Y870D02*
+X1702Y870D01*
+X1638Y869D02*
+X1703Y869D01*
+X1635Y868D02*
+X1702Y868D01*
+X1633Y867D02*
+X1702Y867D01*
+X1630Y866D02*
+X1702Y866D01*
+X1627Y865D02*
+X1701Y865D01*
+X1625Y864D02*
+X1701Y864D01*
+X1622Y863D02*
+X1700Y863D01*
+X1620Y862D02*
+X1699Y862D01*
+X1617Y861D02*
+X1697Y861D01*
+X1615Y860D02*
+X1664Y860D01*
+X1612Y859D02*
+X1661Y859D01*
+X1609Y858D02*
+X1659Y858D01*
+X1607Y857D02*
+X1656Y857D01*
+X1604Y856D02*
+X1653Y856D01*
+X1602Y855D02*
+X1651Y855D01*
+X1599Y854D02*
+X1648Y854D01*
+X1597Y853D02*
+X1646Y853D01*
+X1594Y852D02*
+X1643Y852D01*
+X1592Y851D02*
+X1641Y851D01*
+X1589Y850D02*
+X1638Y850D01*
+X1586Y849D02*
+X1635Y849D01*
+X1584Y848D02*
+X1633Y848D01*
+X1581Y847D02*
+X1630Y847D01*
+X1579Y846D02*
+X1628Y846D01*
+X1576Y845D02*
+X1625Y845D01*
+X1574Y844D02*
+X1623Y844D01*
+X1571Y843D02*
+X1620Y843D01*
+X1569Y842D02*
+X1618Y842D01*
+X1567Y841D02*
+X1615Y841D01*
+X1566Y840D02*
+X1612Y840D01*
+X1565Y839D02*
+X1610Y839D01*
+X1564Y838D02*
+X1607Y838D01*
+X1564Y837D02*
+X1605Y837D01*
+X1563Y836D02*
+X1602Y836D01*
+X1563Y835D02*
+X1600Y835D01*
+X1563Y834D02*
+X1597Y834D01*
+X1563Y833D02*
+X1599Y833D01*
+X1563Y832D02*
+X1601Y832D01*
+X1564Y831D02*
+X1604Y831D01*
+X1564Y830D02*
+X1606Y830D01*
+X1564Y829D02*
+X1609Y829D01*
+X1565Y828D02*
+X1611Y828D01*
+X1566Y827D02*
+X1614Y827D01*
+X1567Y826D02*
+X1616Y826D01*
+X1569Y825D02*
+X1619Y825D01*
+X1572Y824D02*
+X1622Y824D01*
+X1574Y823D02*
+X1624Y823D01*
+X1577Y822D02*
+X1627Y822D01*
+X1580Y821D02*
+X1629Y821D01*
+X1582Y820D02*
+X1632Y820D01*
+X1585Y819D02*
+X1634Y819D01*
+X1587Y818D02*
+X1637Y818D01*
+X1590Y817D02*
+X1639Y817D01*
+X1592Y816D02*
+X1642Y816D01*
+X1595Y815D02*
+X1645Y815D01*
+X1598Y814D02*
+X1647Y814D01*
+X1600Y813D02*
+X1650Y813D01*
+X1603Y812D02*
+X1652Y812D01*
+X1605Y811D02*
+X1655Y811D01*
+X1608Y810D02*
+X1657Y810D01*
+X1610Y809D02*
+X1660Y809D01*
+X1613Y808D02*
+X1662Y808D01*
+X1616Y807D02*
+X1695Y807D01*
+X1618Y806D02*
+X1698Y806D01*
+X1621Y805D02*
+X1699Y805D01*
+X1623Y804D02*
+X1700Y804D01*
+X1626Y803D02*
+X1701Y803D01*
+X1628Y802D02*
+X1702Y802D01*
+X1631Y801D02*
+X1702Y801D01*
+X1634Y800D02*
+X1702Y800D01*
+X1636Y799D02*
+X1702Y799D01*
+X1639Y798D02*
+X1703Y798D01*
+X1641Y797D02*
+X1702Y797D01*
+X1644Y796D02*
+X1702Y796D01*
+X1646Y795D02*
+X1702Y795D01*
+X1649Y794D02*
+X1702Y794D01*
+X1652Y793D02*
+X1701Y793D01*
+X1654Y792D02*
+X1700Y792D01*
+X1657Y791D02*
+X1699Y791D01*
+X1659Y790D02*
+X1698Y790D01*
+X1662Y789D02*
+X1694Y789D01*
+X191Y786D02*
+X194Y786D01*
+X106Y785D02*
+X117Y785D01*
+X189Y785D02*
+X198Y785D01*
+X104Y784D02*
+X119Y784D01*
+X187Y784D02*
+X200Y784D01*
+X102Y783D02*
+X121Y783D01*
+X186Y783D02*
+X202Y783D01*
+X101Y782D02*
+X122Y782D01*
+X186Y782D02*
+X204Y782D01*
+X100Y781D02*
+X123Y781D01*
+X185Y781D02*
+X205Y781D01*
+X99Y780D02*
+X125Y780D01*
+X185Y780D02*
+X206Y780D01*
+X98Y779D02*
+X126Y779D01*
+X185Y779D02*
+X207Y779D01*
+X97Y778D02*
+X127Y778D01*
+X185Y778D02*
+X208Y778D01*
+X97Y777D02*
+X128Y777D01*
+X185Y777D02*
+X208Y777D01*
+X96Y776D02*
+X130Y776D01*
+X185Y776D02*
+X209Y776D01*
+X96Y775D02*
+X131Y775D01*
+X186Y775D02*
+X210Y775D01*
+X96Y774D02*
+X132Y774D01*
+X186Y774D02*
+X210Y774D01*
+X95Y773D02*
+X134Y773D01*
+X187Y773D02*
+X211Y773D01*
+X95Y772D02*
+X135Y772D01*
+X188Y772D02*
+X211Y772D01*
+X95Y771D02*
+X136Y771D01*
+X191Y771D02*
+X211Y771D01*
+X95Y770D02*
+X109Y770D01*
+X113Y770D02*
+X137Y770D01*
+X195Y770D02*
+X211Y770D01*
+X95Y769D02*
+X109Y769D01*
+X114Y769D02*
+X139Y769D01*
+X196Y769D02*
+X212Y769D01*
+X95Y768D02*
+X109Y768D01*
+X116Y768D02*
+X140Y768D01*
+X197Y768D02*
+X212Y768D01*
+X95Y767D02*
+X109Y767D01*
+X117Y767D02*
+X141Y767D01*
+X197Y767D02*
+X212Y767D01*
+X95Y766D02*
+X109Y766D01*
+X118Y766D02*
+X143Y766D01*
+X198Y766D02*
+X212Y766D01*
+X95Y765D02*
+X109Y765D01*
+X120Y765D02*
+X144Y765D01*
+X198Y765D02*
+X212Y765D01*
+X95Y764D02*
+X109Y764D01*
+X121Y764D02*
+X145Y764D01*
+X198Y764D02*
+X212Y764D01*
+X95Y763D02*
+X109Y763D01*
+X122Y763D02*
+X146Y763D01*
+X198Y763D02*
+X212Y763D01*
+X95Y762D02*
+X109Y762D01*
+X123Y762D02*
+X148Y762D01*
+X198Y762D02*
+X212Y762D01*
+X95Y761D02*
+X109Y761D01*
+X125Y761D02*
+X149Y761D01*
+X198Y761D02*
+X212Y761D01*
+X95Y760D02*
+X109Y760D01*
+X126Y760D02*
+X150Y760D01*
+X198Y760D02*
+X212Y760D01*
+X95Y759D02*
+X109Y759D01*
+X127Y759D02*
+X152Y759D01*
+X198Y759D02*
+X212Y759D01*
+X95Y758D02*
+X109Y758D01*
+X129Y758D02*
+X153Y758D01*
+X198Y758D02*
+X212Y758D01*
+X95Y757D02*
+X109Y757D01*
+X130Y757D02*
+X154Y757D01*
+X198Y757D02*
+X212Y757D01*
+X95Y756D02*
+X109Y756D01*
+X131Y756D02*
+X155Y756D01*
+X198Y756D02*
+X212Y756D01*
+X95Y755D02*
+X109Y755D01*
+X132Y755D02*
+X157Y755D01*
+X198Y755D02*
+X212Y755D01*
+X95Y754D02*
+X109Y754D01*
+X134Y754D02*
+X158Y754D01*
+X198Y754D02*
+X212Y754D01*
+X95Y753D02*
+X109Y753D01*
+X135Y753D02*
+X159Y753D01*
+X198Y753D02*
+X212Y753D01*
+X95Y752D02*
+X109Y752D01*
+X136Y752D02*
+X161Y752D01*
+X198Y752D02*
+X212Y752D01*
+X95Y751D02*
+X109Y751D01*
+X138Y751D02*
+X162Y751D01*
+X198Y751D02*
+X212Y751D01*
+X95Y750D02*
+X109Y750D01*
+X139Y750D02*
+X163Y750D01*
+X198Y750D02*
+X212Y750D01*
+X95Y749D02*
+X109Y749D01*
+X140Y749D02*
+X164Y749D01*
+X198Y749D02*
+X212Y749D01*
+X95Y748D02*
+X109Y748D01*
+X141Y748D02*
+X166Y748D01*
+X198Y748D02*
+X212Y748D01*
+X1569Y748D02*
+X1620Y748D01*
+X95Y747D02*
+X109Y747D01*
+X143Y747D02*
+X167Y747D01*
+X198Y747D02*
+X212Y747D01*
+X1567Y747D02*
+X1622Y747D01*
+X95Y746D02*
+X109Y746D01*
+X144Y746D02*
+X168Y746D01*
+X198Y746D02*
+X212Y746D01*
+X1566Y746D02*
+X1623Y746D01*
+X95Y745D02*
+X109Y745D01*
+X145Y745D02*
+X170Y745D01*
+X198Y745D02*
+X212Y745D01*
+X1565Y745D02*
+X1624Y745D01*
+X95Y744D02*
+X109Y744D01*
+X147Y744D02*
+X171Y744D01*
+X198Y744D02*
+X212Y744D01*
+X1565Y744D02*
+X1625Y744D01*
+X95Y743D02*
+X109Y743D01*
+X148Y743D02*
+X172Y743D01*
+X198Y743D02*
+X212Y743D01*
+X1564Y743D02*
+X1626Y743D01*
+X95Y742D02*
+X109Y742D01*
+X149Y742D02*
+X173Y742D01*
+X198Y742D02*
+X212Y742D01*
+X1564Y742D02*
+X1626Y742D01*
+X95Y741D02*
+X109Y741D01*
+X151Y741D02*
+X175Y741D01*
+X198Y741D02*
+X212Y741D01*
+X1563Y741D02*
+X1626Y741D01*
+X95Y740D02*
+X109Y740D01*
+X152Y740D02*
+X176Y740D01*
+X198Y740D02*
+X212Y740D01*
+X1563Y740D02*
+X1626Y740D01*
+X95Y739D02*
+X109Y739D01*
+X153Y739D02*
+X177Y739D01*
+X198Y739D02*
+X212Y739D01*
+X1563Y739D02*
+X1626Y739D01*
+X95Y738D02*
+X109Y738D01*
+X154Y738D02*
+X179Y738D01*
+X198Y738D02*
+X212Y738D01*
+X1563Y738D02*
+X1626Y738D01*
+X95Y737D02*
+X109Y737D01*
+X156Y737D02*
+X180Y737D01*
+X198Y737D02*
+X212Y737D01*
+X1563Y737D02*
+X1626Y737D01*
+X95Y736D02*
+X109Y736D01*
+X157Y736D02*
+X181Y736D01*
+X198Y736D02*
+X212Y736D01*
+X1563Y736D02*
+X1626Y736D01*
+X95Y735D02*
+X109Y735D01*
+X158Y735D02*
+X182Y735D01*
+X198Y735D02*
+X212Y735D01*
+X1563Y735D02*
+X1626Y735D01*
+X95Y734D02*
+X109Y734D01*
+X160Y734D02*
+X184Y734D01*
+X198Y734D02*
+X212Y734D01*
+X1563Y734D02*
+X1626Y734D01*
+X95Y733D02*
+X109Y733D01*
+X161Y733D02*
+X185Y733D01*
+X198Y733D02*
+X212Y733D01*
+X1563Y733D02*
+X1626Y733D01*
+X95Y732D02*
+X109Y732D01*
+X162Y732D02*
+X186Y732D01*
+X198Y732D02*
+X212Y732D01*
+X1563Y732D02*
+X1626Y732D01*
+X95Y731D02*
+X109Y731D01*
+X163Y731D02*
+X188Y731D01*
+X198Y731D02*
+X212Y731D01*
+X1563Y731D02*
+X1626Y731D01*
+X95Y730D02*
+X109Y730D01*
+X165Y730D02*
+X189Y730D01*
+X198Y730D02*
+X212Y730D01*
+X1563Y730D02*
+X1581Y730D01*
+X1609Y730D02*
+X1626Y730D01*
+X95Y729D02*
+X110Y729D01*
+X166Y729D02*
+X190Y729D01*
+X198Y729D02*
+X212Y729D01*
+X1563Y729D02*
+X1580Y729D01*
+X1609Y729D02*
+X1626Y729D01*
+X95Y728D02*
+X110Y728D01*
+X167Y728D02*
+X191Y728D01*
+X198Y728D02*
+X212Y728D01*
+X1563Y728D02*
+X1580Y728D01*
+X1609Y728D02*
+X1626Y728D01*
+X95Y727D02*
+X111Y727D01*
+X169Y727D02*
+X193Y727D01*
+X198Y727D02*
+X212Y727D01*
+X1563Y727D02*
+X1580Y727D01*
+X1609Y727D02*
+X1626Y727D01*
+X96Y726D02*
+X114Y726D01*
+X170Y726D02*
+X194Y726D01*
+X196Y726D02*
+X212Y726D01*
+X1563Y726D02*
+X1580Y726D01*
+X1609Y726D02*
+X1626Y726D01*
+X96Y725D02*
+X118Y725D01*
+X171Y725D02*
+X212Y725D01*
+X1563Y725D02*
+X1580Y725D01*
+X1609Y725D02*
+X1626Y725D01*
+X96Y724D02*
+X119Y724D01*
+X172Y724D02*
+X212Y724D01*
+X1563Y724D02*
+X1580Y724D01*
+X1609Y724D02*
+X1626Y724D01*
+X97Y723D02*
+X120Y723D01*
+X174Y723D02*
+X211Y723D01*
+X1563Y723D02*
+X1580Y723D01*
+X1609Y723D02*
+X1626Y723D01*
+X97Y722D02*
+X121Y722D01*
+X175Y722D02*
+X211Y722D01*
+X1563Y722D02*
+X1580Y722D01*
+X1609Y722D02*
+X1626Y722D01*
+X98Y721D02*
+X122Y721D01*
+X176Y721D02*
+X211Y721D01*
+X1563Y721D02*
+X1580Y721D01*
+X1609Y721D02*
+X1626Y721D01*
+X98Y720D02*
+X122Y720D01*
+X178Y720D02*
+X210Y720D01*
+X1563Y720D02*
+X1580Y720D01*
+X1609Y720D02*
+X1626Y720D01*
+X99Y719D02*
+X122Y719D01*
+X179Y719D02*
+X210Y719D01*
+X1563Y719D02*
+X1580Y719D01*
+X1609Y719D02*
+X1626Y719D01*
+X100Y718D02*
+X122Y718D01*
+X180Y718D02*
+X209Y718D01*
+X1563Y718D02*
+X1580Y718D01*
+X1609Y718D02*
+X1626Y718D01*
+X101Y717D02*
+X122Y717D01*
+X181Y717D02*
+X208Y717D01*
+X1563Y717D02*
+X1580Y717D01*
+X1609Y717D02*
+X1626Y717D01*
+X102Y716D02*
+X122Y716D01*
+X183Y716D02*
+X207Y716D01*
+X1563Y716D02*
+X1580Y716D01*
+X1609Y716D02*
+X1626Y716D01*
+X103Y715D02*
+X121Y715D01*
+X184Y715D02*
+X206Y715D01*
+X1563Y715D02*
+X1580Y715D01*
+X1609Y715D02*
+X1626Y715D01*
+X104Y714D02*
+X121Y714D01*
+X185Y714D02*
+X205Y714D01*
+X1563Y714D02*
+X1580Y714D01*
+X1609Y714D02*
+X1626Y714D01*
+X106Y713D02*
+X120Y713D01*
+X187Y713D02*
+X204Y713D01*
+X1563Y713D02*
+X1580Y713D01*
+X1609Y713D02*
+X1626Y713D01*
+X108Y712D02*
+X119Y712D01*
+X189Y712D02*
+X202Y712D01*
+X1563Y712D02*
+X1580Y712D01*
+X1609Y712D02*
+X1626Y712D01*
+X112Y711D02*
+X117Y711D01*
+X192Y711D02*
+X198Y711D01*
+X1563Y711D02*
+X1580Y711D01*
+X1609Y711D02*
+X1626Y711D01*
+X1563Y710D02*
+X1580Y710D01*
+X1609Y710D02*
+X1626Y710D01*
+X1563Y709D02*
+X1580Y709D01*
+X1609Y709D02*
+X1626Y709D01*
+X1563Y708D02*
+X1580Y708D01*
+X1609Y708D02*
+X1626Y708D01*
+X1563Y707D02*
+X1580Y707D01*
+X1609Y707D02*
+X1626Y707D01*
+X1563Y706D02*
+X1580Y706D01*
+X1609Y706D02*
+X1626Y706D01*
+X1563Y705D02*
+X1580Y705D01*
+X1609Y705D02*
+X1626Y705D01*
+X1563Y704D02*
+X1580Y704D01*
+X1609Y704D02*
+X1626Y704D01*
+X1563Y703D02*
+X1580Y703D01*
+X1609Y703D02*
+X1626Y703D01*
+X1563Y702D02*
+X1580Y702D01*
+X1609Y702D02*
+X1626Y702D01*
+X1563Y701D02*
+X1580Y701D01*
+X1609Y701D02*
+X1626Y701D01*
+X1563Y700D02*
+X1580Y700D01*
+X1609Y700D02*
+X1626Y700D01*
+X1563Y699D02*
+X1580Y699D01*
+X1609Y699D02*
+X1626Y699D01*
+X1563Y698D02*
+X1580Y698D01*
+X1609Y698D02*
+X1626Y698D01*
+X1563Y697D02*
+X1580Y697D01*
+X1609Y697D02*
+X1626Y697D01*
+X1563Y696D02*
+X1580Y696D01*
+X1609Y696D02*
+X1626Y696D01*
+X1563Y695D02*
+X1580Y695D01*
+X1609Y695D02*
+X1626Y695D01*
+X1563Y694D02*
+X1580Y694D01*
+X1609Y694D02*
+X1626Y694D01*
+X1563Y693D02*
+X1580Y693D01*
+X1609Y693D02*
+X1626Y693D01*
+X1563Y692D02*
+X1580Y692D01*
+X1609Y692D02*
+X1626Y692D01*
+X1563Y691D02*
+X1580Y691D01*
+X1609Y691D02*
+X1626Y691D01*
+X1563Y690D02*
+X1580Y690D01*
+X1609Y690D02*
+X1626Y690D01*
+X1563Y689D02*
+X1580Y689D01*
+X1609Y689D02*
+X1626Y689D01*
+X1563Y688D02*
+X1580Y688D01*
+X1609Y688D02*
+X1626Y688D01*
+X1563Y687D02*
+X1580Y687D01*
+X1609Y687D02*
+X1626Y687D01*
+X1563Y686D02*
+X1580Y686D01*
+X1609Y686D02*
+X1626Y686D01*
+X1563Y685D02*
+X1580Y685D01*
+X1609Y685D02*
+X1626Y685D01*
+X1690Y685D02*
+X1698Y685D01*
+X1563Y684D02*
+X1580Y684D01*
+X1609Y684D02*
+X1626Y684D01*
+X1689Y684D02*
+X1699Y684D01*
+X1563Y683D02*
+X1580Y683D01*
+X1609Y683D02*
+X1626Y683D01*
+X1688Y683D02*
+X1700Y683D01*
+X1563Y682D02*
+X1580Y682D01*
+X1609Y682D02*
+X1626Y682D01*
+X1687Y682D02*
+X1701Y682D01*
+X1563Y681D02*
+X1580Y681D01*
+X1609Y681D02*
+X1626Y681D01*
+X1686Y681D02*
+X1702Y681D01*
+X1563Y680D02*
+X1580Y680D01*
+X1609Y680D02*
+X1626Y680D01*
+X1686Y680D02*
+X1702Y680D01*
+X1563Y679D02*
+X1580Y679D01*
+X1609Y679D02*
+X1626Y679D01*
+X1686Y679D02*
+X1702Y679D01*
+X1563Y678D02*
+X1580Y678D01*
+X1609Y678D02*
+X1626Y678D01*
+X1685Y678D02*
+X1702Y678D01*
+X1563Y677D02*
+X1581Y677D01*
+X1609Y677D02*
+X1627Y677D01*
+X1685Y677D02*
+X1703Y677D01*
+X1563Y676D02*
+X1703Y676D01*
+X1563Y675D02*
+X1703Y675D01*
+X1563Y674D02*
+X1703Y674D01*
+X1563Y673D02*
+X1703Y673D01*
+X1563Y672D02*
+X1703Y672D01*
+X1563Y671D02*
+X1703Y671D01*
+X1563Y670D02*
+X1703Y670D01*
+X1563Y669D02*
+X1703Y669D01*
+X1563Y668D02*
+X1703Y668D01*
+X1563Y667D02*
+X1702Y667D01*
+X1563Y666D02*
+X1702Y666D01*
+X1564Y665D02*
+X1702Y665D01*
+X1564Y664D02*
+X1702Y664D01*
+X1565Y663D02*
+X1701Y663D01*
+X1566Y662D02*
+X1700Y662D01*
+X1567Y661D02*
+X1699Y661D01*
+X1568Y660D02*
+X1698Y660D01*
+X1572Y659D02*
+X1694Y659D01*
+X1623Y618D02*
+X1635Y618D01*
+X1621Y617D02*
+X1637Y617D01*
+X1620Y616D02*
+X1639Y616D01*
+X1619Y615D02*
+X1640Y615D01*
+X1618Y614D02*
+X1640Y614D01*
+X1617Y613D02*
+X1641Y613D01*
+X1617Y612D02*
+X1641Y612D01*
+X1617Y611D02*
+X1641Y611D01*
+X1617Y610D02*
+X1642Y610D01*
+X1617Y609D02*
+X1642Y609D01*
+X1617Y608D02*
+X1642Y608D01*
+X1617Y607D02*
+X1642Y607D01*
+X1617Y606D02*
+X1642Y606D01*
+X1617Y605D02*
+X1642Y605D01*
+X1617Y604D02*
+X1642Y604D01*
+X1617Y603D02*
+X1642Y603D01*
+X1617Y602D02*
+X1642Y602D01*
+X1617Y601D02*
+X1642Y601D01*
+X1617Y600D02*
+X1642Y600D01*
+X1617Y599D02*
+X1642Y599D01*
+X1617Y598D02*
+X1642Y598D01*
+X1617Y597D02*
+X1642Y597D01*
+X1617Y596D02*
+X1642Y596D01*
+X1617Y595D02*
+X1642Y595D01*
+X1617Y594D02*
+X1642Y594D01*
+X1617Y593D02*
+X1642Y593D01*
+X1617Y592D02*
+X1642Y592D01*
+X1617Y591D02*
+X1642Y591D01*
+X1617Y590D02*
+X1642Y590D01*
+X1617Y589D02*
+X1642Y589D01*
+X1617Y588D02*
+X1642Y588D01*
+X1617Y587D02*
+X1642Y587D01*
+X1617Y586D02*
+X1642Y586D01*
+X1617Y585D02*
+X1642Y585D01*
+X1617Y584D02*
+X1642Y584D01*
+X1617Y583D02*
+X1642Y583D01*
+X1617Y582D02*
+X1642Y582D01*
+X1617Y581D02*
+X1642Y581D01*
+X1617Y580D02*
+X1642Y580D01*
+X1617Y579D02*
+X1642Y579D01*
+X1617Y578D02*
+X1642Y578D01*
+X1617Y577D02*
+X1642Y577D01*
+X1617Y576D02*
+X1642Y576D01*
+X1617Y575D02*
+X1642Y575D01*
+X1617Y574D02*
+X1642Y574D01*
+X1617Y573D02*
+X1642Y573D01*
+X1617Y572D02*
+X1642Y572D01*
+X1617Y571D02*
+X1642Y571D01*
+X1617Y570D02*
+X1642Y570D01*
+X1617Y569D02*
+X1642Y569D01*
+X1617Y568D02*
+X1642Y568D01*
+X1617Y567D02*
+X1642Y567D01*
+X1617Y566D02*
+X1642Y566D01*
+X1617Y565D02*
+X1642Y565D01*
+X1617Y564D02*
+X1642Y564D01*
+X1617Y563D02*
+X1642Y563D01*
+X1617Y562D02*
+X1642Y562D01*
+X1617Y561D02*
+X1642Y561D01*
+X1617Y560D02*
+X1642Y560D01*
+X1617Y559D02*
+X1642Y559D01*
+X1617Y558D02*
+X1642Y558D01*
+X1617Y557D02*
+X1642Y557D01*
+X1617Y556D02*
+X1642Y556D01*
+X1617Y555D02*
+X1642Y555D01*
+X1617Y554D02*
+X1642Y554D01*
+X1617Y553D02*
+X1642Y553D01*
+X1617Y552D02*
+X1642Y552D01*
+X1617Y551D02*
+X1642Y551D01*
+X1617Y550D02*
+X1642Y550D01*
+X1617Y549D02*
+X1642Y549D01*
+X1617Y548D02*
+X1642Y548D01*
+X1617Y547D02*
+X1642Y547D01*
+X1617Y546D02*
+X1642Y546D01*
+X1617Y545D02*
+X1642Y545D01*
+X1617Y544D02*
+X1642Y544D01*
+X1617Y543D02*
+X1642Y543D01*
+X1617Y542D02*
+X1642Y542D01*
+X1617Y541D02*
+X1642Y541D01*
+X1617Y540D02*
+X1642Y540D01*
+X1617Y539D02*
+X1642Y539D01*
+X1617Y538D02*
+X1642Y538D01*
+X1617Y537D02*
+X1642Y537D01*
+X1617Y536D02*
+X1641Y536D01*
+X1617Y535D02*
+X1641Y535D01*
+X1618Y534D02*
+X1641Y534D01*
+X1618Y533D02*
+X1640Y533D01*
+X1619Y532D02*
+X1639Y532D01*
+X1620Y531D02*
+X1638Y531D01*
+X1621Y530D02*
+X1637Y530D01*
+X1625Y529D02*
+X1633Y529D01*
+X1627Y488D02*
+X1638Y488D01*
+X1623Y487D02*
+X1643Y487D01*
+X1620Y486D02*
+X1646Y486D01*
+X1617Y485D02*
+X1649Y485D01*
+X1615Y484D02*
+X1651Y484D01*
+X1613Y483D02*
+X1653Y483D01*
+X1611Y482D02*
+X1655Y482D01*
+X1609Y481D02*
+X1657Y481D01*
+X1607Y480D02*
+X1659Y480D01*
+X1605Y479D02*
+X1661Y479D01*
+X1603Y478D02*
+X1663Y478D01*
+X1601Y477D02*
+X1665Y477D01*
+X1599Y476D02*
+X1667Y476D01*
+X1597Y475D02*
+X1669Y475D01*
+X1595Y474D02*
+X1671Y474D01*
+X1593Y473D02*
+X1673Y473D01*
+X1591Y472D02*
+X1675Y472D01*
+X1589Y471D02*
+X1677Y471D01*
+X1587Y470D02*
+X1629Y470D01*
+X1637Y470D02*
+X1679Y470D01*
+X1585Y469D02*
+X1625Y469D01*
+X1641Y469D02*
+X1681Y469D01*
+X1583Y468D02*
+X1622Y468D01*
+X1643Y468D02*
+X1683Y468D01*
+X1581Y467D02*
+X1620Y467D01*
+X1645Y467D02*
+X1685Y467D01*
+X1579Y466D02*
+X1618Y466D01*
+X1647Y466D02*
+X1687Y466D01*
+X1577Y465D02*
+X1616Y465D01*
+X1649Y465D02*
+X1689Y465D01*
+X1575Y464D02*
+X1614Y464D01*
+X1651Y464D02*
+X1690Y464D01*
+X1573Y463D02*
+X1612Y463D01*
+X1653Y463D02*
+X1692Y463D01*
+X1572Y462D02*
+X1611Y462D01*
+X1655Y462D02*
+X1693Y462D01*
+X1571Y461D02*
+X1609Y461D01*
+X1657Y461D02*
+X1694Y461D01*
+X1570Y460D02*
+X1607Y460D01*
+X1659Y460D02*
+X1695Y460D01*
+X1569Y459D02*
+X1605Y459D01*
+X1661Y459D02*
+X1696Y459D01*
+X1569Y458D02*
+X1603Y458D01*
+X1663Y458D02*
+X1697Y458D01*
+X1568Y457D02*
+X1601Y457D01*
+X1665Y457D02*
+X1697Y457D01*
+X1567Y456D02*
+X1599Y456D01*
+X1667Y456D02*
+X1698Y456D01*
+X1567Y455D02*
+X1597Y455D01*
+X1669Y455D02*
+X1699Y455D01*
+X1566Y454D02*
+X1595Y454D01*
+X1671Y454D02*
+X1699Y454D01*
+X1566Y453D02*
+X1593Y453D01*
+X1673Y453D02*
+X1700Y453D01*
+X1565Y452D02*
+X1591Y452D01*
+X1675Y452D02*
+X1700Y452D01*
+X1565Y451D02*
+X1589Y451D01*
+X1677Y451D02*
+X1701Y451D01*
+X1565Y450D02*
+X1587Y450D01*
+X1679Y450D02*
+X1701Y450D01*
+X1564Y449D02*
+X1585Y449D01*
+X1681Y449D02*
+X1701Y449D01*
+X1564Y448D02*
+X1583Y448D01*
+X1682Y448D02*
+X1702Y448D01*
+X1564Y447D02*
+X1582Y447D01*
+X1683Y447D02*
+X1702Y447D01*
+X1564Y446D02*
+X1582Y446D01*
+X1684Y446D02*
+X1702Y446D01*
+X1563Y445D02*
+X1581Y445D01*
+X1685Y445D02*
+X1702Y445D01*
+X1563Y444D02*
+X1581Y444D01*
+X1685Y444D02*
+X1702Y444D01*
+X1563Y443D02*
+X1581Y443D01*
+X1685Y443D02*
+X1702Y443D01*
+X1563Y442D02*
+X1580Y442D01*
+X1685Y442D02*
+X1703Y442D01*
+X1563Y441D02*
+X1580Y441D01*
+X1685Y441D02*
+X1703Y441D01*
+X1563Y440D02*
+X1580Y440D01*
+X1685Y440D02*
+X1703Y440D01*
+X1563Y439D02*
+X1580Y439D01*
+X1685Y439D02*
+X1703Y439D01*
+X1563Y438D02*
+X1580Y438D01*
+X1685Y438D02*
+X1703Y438D01*
+X1563Y437D02*
+X1580Y437D01*
+X1685Y437D02*
+X1703Y437D01*
+X1563Y436D02*
+X1580Y436D01*
+X1685Y436D02*
+X1703Y436D01*
+X1563Y435D02*
+X1581Y435D01*
+X1685Y435D02*
+X1703Y435D01*
+X1563Y434D02*
+X1703Y434D01*
+X99Y433D02*
+X105Y433D01*
+X202Y433D02*
+X208Y433D01*
+X1563Y433D02*
+X1703Y433D01*
+X98Y432D02*
+X106Y432D01*
+X200Y432D02*
+X209Y432D01*
+X1563Y432D02*
+X1703Y432D01*
+X97Y431D02*
+X107Y431D01*
+X199Y431D02*
+X210Y431D01*
+X1563Y431D02*
+X1703Y431D01*
+X96Y430D02*
+X108Y430D01*
+X199Y430D02*
+X211Y430D01*
+X1563Y430D02*
+X1703Y430D01*
+X95Y429D02*
+X109Y429D01*
+X198Y429D02*
+X211Y429D01*
+X1563Y429D02*
+X1703Y429D01*
+X95Y428D02*
+X109Y428D01*
+X198Y428D02*
+X212Y428D01*
+X1563Y428D02*
+X1703Y428D01*
+X95Y427D02*
+X109Y427D01*
+X198Y427D02*
+X212Y427D01*
+X1563Y427D02*
+X1703Y427D01*
+X95Y426D02*
+X109Y426D01*
+X198Y426D02*
+X212Y426D01*
+X1563Y426D02*
+X1703Y426D01*
+X95Y425D02*
+X109Y425D01*
+X198Y425D02*
+X212Y425D01*
+X1563Y425D02*
+X1703Y425D01*
+X95Y424D02*
+X109Y424D01*
+X198Y424D02*
+X212Y424D01*
+X1563Y424D02*
+X1703Y424D01*
+X95Y423D02*
+X109Y423D01*
+X198Y423D02*
+X212Y423D01*
+X1563Y423D02*
+X1703Y423D01*
+X95Y422D02*
+X109Y422D01*
+X198Y422D02*
+X212Y422D01*
+X1563Y422D02*
+X1703Y422D01*
+X95Y421D02*
+X109Y421D01*
+X198Y421D02*
+X212Y421D01*
+X1563Y421D02*
+X1703Y421D01*
+X95Y420D02*
+X109Y420D01*
+X198Y420D02*
+X212Y420D01*
+X1563Y420D02*
+X1703Y420D01*
+X95Y419D02*
+X109Y419D01*
+X198Y419D02*
+X212Y419D01*
+X1563Y419D02*
+X1703Y419D01*
+X95Y418D02*
+X109Y418D01*
+X198Y418D02*
+X212Y418D01*
+X1563Y418D02*
+X1703Y418D01*
+X95Y417D02*
+X109Y417D01*
+X198Y417D02*
+X212Y417D01*
+X1563Y417D02*
+X1703Y417D01*
+X95Y416D02*
+X109Y416D01*
+X198Y416D02*
+X212Y416D01*
+X1563Y416D02*
+X1580Y416D01*
+X1685Y416D02*
+X1703Y416D01*
+X95Y415D02*
+X109Y415D01*
+X198Y415D02*
+X212Y415D01*
+X1563Y415D02*
+X1580Y415D01*
+X1685Y415D02*
+X1703Y415D01*
+X95Y414D02*
+X109Y414D01*
+X198Y414D02*
+X212Y414D01*
+X1563Y414D02*
+X1580Y414D01*
+X1685Y414D02*
+X1703Y414D01*
+X95Y413D02*
+X109Y413D01*
+X198Y413D02*
+X212Y413D01*
+X1563Y413D02*
+X1580Y413D01*
+X1685Y413D02*
+X1703Y413D01*
+X95Y412D02*
+X109Y412D01*
+X198Y412D02*
+X212Y412D01*
+X1563Y412D02*
+X1580Y412D01*
+X1685Y412D02*
+X1703Y412D01*
+X95Y411D02*
+X109Y411D01*
+X198Y411D02*
+X212Y411D01*
+X1563Y411D02*
+X1580Y411D01*
+X1685Y411D02*
+X1703Y411D01*
+X95Y410D02*
+X109Y410D01*
+X198Y410D02*
+X212Y410D01*
+X1563Y410D02*
+X1580Y410D01*
+X1685Y410D02*
+X1703Y410D01*
+X95Y409D02*
+X109Y409D01*
+X198Y409D02*
+X212Y409D01*
+X1563Y409D02*
+X1580Y409D01*
+X1685Y409D02*
+X1703Y409D01*
+X95Y408D02*
+X109Y408D01*
+X198Y408D02*
+X212Y408D01*
+X1563Y408D02*
+X1580Y408D01*
+X1685Y408D02*
+X1703Y408D01*
+X95Y407D02*
+X109Y407D01*
+X198Y407D02*
+X212Y407D01*
+X1563Y407D02*
+X1580Y407D01*
+X1685Y407D02*
+X1702Y407D01*
+X95Y406D02*
+X109Y406D01*
+X198Y406D02*
+X212Y406D01*
+X1563Y406D02*
+X1580Y406D01*
+X1686Y406D02*
+X1702Y406D01*
+X95Y405D02*
+X109Y405D01*
+X198Y405D02*
+X212Y405D01*
+X1564Y405D02*
+X1580Y405D01*
+X1686Y405D02*
+X1702Y405D01*
+X95Y404D02*
+X109Y404D01*
+X198Y404D02*
+X212Y404D01*
+X1564Y404D02*
+X1580Y404D01*
+X1686Y404D02*
+X1702Y404D01*
+X95Y403D02*
+X109Y403D01*
+X198Y403D02*
+X212Y403D01*
+X1565Y403D02*
+X1579Y403D01*
+X1687Y403D02*
+X1701Y403D01*
+X95Y402D02*
+X109Y402D01*
+X198Y402D02*
+X212Y402D01*
+X1565Y402D02*
+X1578Y402D01*
+X1688Y402D02*
+X1700Y402D01*
+X95Y401D02*
+X109Y401D01*
+X198Y401D02*
+X212Y401D01*
+X1567Y401D02*
+X1577Y401D01*
+X1689Y401D02*
+X1699Y401D01*
+X95Y400D02*
+X109Y400D01*
+X198Y400D02*
+X212Y400D01*
+X1568Y400D02*
+X1576Y400D01*
+X1690Y400D02*
+X1698Y400D01*
+X95Y399D02*
+X109Y399D01*
+X198Y399D02*
+X212Y399D01*
+X1571Y399D02*
+X1573Y399D01*
+X1693Y399D02*
+X1695Y399D01*
+X95Y398D02*
+X109Y398D01*
+X198Y398D02*
+X212Y398D01*
+X95Y397D02*
+X109Y397D01*
+X198Y397D02*
+X212Y397D01*
+X95Y396D02*
+X109Y396D01*
+X197Y396D02*
+X212Y396D01*
+X95Y395D02*
+X110Y395D01*
+X197Y395D02*
+X212Y395D01*
+X95Y394D02*
+X110Y394D01*
+X197Y394D02*
+X212Y394D01*
+X95Y393D02*
+X111Y393D01*
+X196Y393D02*
+X211Y393D01*
+X96Y392D02*
+X112Y392D01*
+X195Y392D02*
+X211Y392D01*
+X96Y391D02*
+X114Y391D01*
+X193Y391D02*
+X211Y391D01*
+X96Y390D02*
+X116Y390D01*
+X191Y390D02*
+X211Y390D01*
+X97Y389D02*
+X118Y389D01*
+X189Y389D02*
+X210Y389D01*
+X97Y388D02*
+X120Y388D01*
+X187Y388D02*
+X210Y388D01*
+X98Y387D02*
+X122Y387D01*
+X185Y387D02*
+X209Y387D01*
+X98Y386D02*
+X124Y386D01*
+X183Y386D02*
+X209Y386D01*
+X99Y385D02*
+X126Y385D01*
+X181Y385D02*
+X208Y385D01*
+X100Y384D02*
+X128Y384D01*
+X179Y384D02*
+X207Y384D01*
+X101Y383D02*
+X130Y383D01*
+X177Y383D02*
+X207Y383D01*
+X101Y382D02*
+X132Y382D01*
+X175Y382D02*
+X206Y382D01*
+X102Y381D02*
+X134Y381D01*
+X173Y381D02*
+X205Y381D01*
+X104Y380D02*
+X136Y380D01*
+X171Y380D02*
+X204Y380D01*
+X105Y379D02*
+X138Y379D01*
+X169Y379D02*
+X202Y379D01*
+X107Y378D02*
+X140Y378D01*
+X167Y378D02*
+X201Y378D01*
+X108Y377D02*
+X141Y377D01*
+X165Y377D02*
+X199Y377D01*
+X110Y376D02*
+X143Y376D01*
+X163Y376D02*
+X197Y376D01*
+X112Y375D02*
+X146Y375D01*
+X161Y375D02*
+X195Y375D01*
+X114Y374D02*
+X149Y374D01*
+X157Y374D02*
+X193Y374D01*
+X116Y373D02*
+X191Y373D01*
+X118Y372D02*
+X189Y372D01*
+X120Y371D02*
+X187Y371D01*
+X122Y370D02*
+X185Y370D01*
+X124Y369D02*
+X183Y369D01*
+X126Y368D02*
+X181Y368D01*
+X128Y367D02*
+X179Y367D01*
+X130Y366D02*
+X177Y366D01*
+X132Y365D02*
+X174Y365D01*
+X134Y364D02*
+X172Y364D01*
+X136Y363D02*
+X170Y363D01*
+X138Y362D02*
+X168Y362D01*
+X141Y361D02*
+X166Y361D01*
+X144Y360D02*
+X163Y360D01*
+X148Y359D02*
+X159Y359D01*
+X1569Y358D02*
+X1702Y358D01*
+X1567Y357D02*
+X1703Y357D01*
+X1566Y356D02*
+X1703Y356D01*
+X1565Y355D02*
+X1703Y355D01*
+X1565Y354D02*
+X1703Y354D01*
+X1564Y353D02*
+X1703Y353D01*
+X1564Y352D02*
+X1703Y352D01*
+X1563Y351D02*
+X1703Y351D01*
+X1563Y350D02*
+X1703Y350D01*
+X1563Y349D02*
+X1703Y349D01*
+X1563Y348D02*
+X1703Y348D01*
+X1564Y347D02*
+X1703Y347D01*
+X1564Y346D02*
+X1703Y346D01*
+X1564Y345D02*
+X1703Y345D01*
+X1565Y344D02*
+X1703Y344D01*
+X1566Y343D02*
+X1703Y343D01*
+X1567Y342D02*
+X1703Y342D01*
+X1569Y341D02*
+X1703Y341D01*
+X1678Y340D02*
+X1703Y340D01*
+X1677Y339D02*
+X1703Y339D01*
+X1675Y338D02*
+X1703Y338D01*
+X1674Y337D02*
+X1703Y337D01*
+X1672Y336D02*
+X1703Y336D01*
+X1671Y335D02*
+X1702Y335D01*
+X1670Y334D02*
+X1700Y334D01*
+X1668Y333D02*
+X1699Y333D01*
+X1667Y332D02*
+X1697Y332D01*
+X1665Y331D02*
+X1696Y331D01*
+X1664Y330D02*
+X1694Y330D01*
+X1662Y329D02*
+X1693Y329D01*
+X1661Y328D02*
+X1692Y328D01*
+X1660Y327D02*
+X1690Y327D01*
+X1658Y326D02*
+X1689Y326D01*
+X1657Y325D02*
+X1687Y325D01*
+X1655Y324D02*
+X1686Y324D01*
+X1654Y323D02*
+X1684Y323D01*
+X1645Y322D02*
+X1683Y322D01*
+X1643Y321D02*
+X1682Y321D01*
+X1642Y320D02*
+X1680Y320D01*
+X1641Y319D02*
+X1679Y319D01*
+X1641Y318D02*
+X1677Y318D01*
+X1640Y317D02*
+X1676Y317D01*
+X1640Y316D02*
+X1674Y316D01*
+X1640Y315D02*
+X1673Y315D01*
+X1640Y314D02*
+X1672Y314D01*
+X1640Y313D02*
+X1672Y313D01*
+X1640Y312D02*
+X1673Y312D01*
+X1640Y311D02*
+X1675Y311D01*
+X1640Y310D02*
+X1676Y310D01*
+X1641Y309D02*
+X1678Y309D01*
+X1641Y308D02*
+X1679Y308D01*
+X1642Y307D02*
+X1681Y307D01*
+X1644Y306D02*
+X1682Y306D01*
+X1646Y305D02*
+X1683Y305D01*
+X1654Y304D02*
+X1685Y304D01*
+X1655Y303D02*
+X1686Y303D01*
+X1657Y302D02*
+X1688Y302D01*
+X1658Y301D02*
+X1689Y301D01*
+X1660Y300D02*
+X1691Y300D01*
+X1661Y299D02*
+X1692Y299D01*
+X1663Y298D02*
+X1693Y298D01*
+X1664Y297D02*
+X1695Y297D01*
+X1665Y296D02*
+X1696Y296D01*
+X1667Y295D02*
+X1698Y295D01*
+X1668Y294D02*
+X1699Y294D01*
+X1670Y293D02*
+X1700Y293D01*
+X1671Y292D02*
+X1702Y292D01*
+X1673Y291D02*
+X1703Y291D01*
+X1674Y290D02*
+X1703Y290D01*
+X1675Y289D02*
+X1703Y289D01*
+X1677Y288D02*
+X1703Y288D01*
+X1571Y287D02*
+X1703Y287D01*
+X1568Y286D02*
+X1703Y286D01*
+X1567Y285D02*
+X1703Y285D01*
+X1566Y284D02*
+X1703Y284D01*
+X1565Y283D02*
+X1703Y283D01*
+X1564Y282D02*
+X1703Y282D01*
+X1564Y281D02*
+X1703Y281D01*
+X1563Y280D02*
+X1703Y280D01*
+X1563Y279D02*
+X1703Y279D01*
+X1563Y278D02*
+X1703Y278D01*
+X1563Y277D02*
+X1703Y277D01*
+X1563Y276D02*
+X1703Y276D01*
+X1564Y275D02*
+X1703Y275D01*
+X1564Y274D02*
+X1703Y274D01*
+X1565Y273D02*
+X1703Y273D01*
+X1565Y272D02*
+X1703Y272D01*
+X1567Y271D02*
+X1703Y271D01*
+X1568Y270D02*
+X1703Y270D01*
+X1571Y269D02*
+X1702Y269D01*
+D02*
+G04 End of Copper0*
+M02*

+ 71 - 0
tests/gerber_files/detector_copper_top.gbr

@@ -0,0 +1,71 @@
+G04 MADE WITH FRITZING*
+G04 WWW.FRITZING.ORG*
+G04 DOUBLE SIDED*
+G04 HOLES PLATED*
+G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
+%ASAXBY*%
+%FSLAX23Y23*%
+%MOIN*%
+%OFA0B0*%
+%SFA1.0B1.0*%
+%ADD10C,0.075000*%
+%ADD11C,0.099055*%
+%ADD12C,0.078740*%
+%ADD13R,0.075000X0.075000*%
+%ADD14C,0.024000*%
+%ADD15C,0.020000*%
+%LNCOPPER1*%
+G90*
+G70*
+G54D10*
+X1149Y872D03*
+X1349Y872D03*
+X749Y722D03*
+X749Y522D03*
+X1149Y522D03*
+X1449Y522D03*
+X1149Y422D03*
+X1449Y422D03*
+X1149Y322D03*
+X1449Y322D03*
+X1149Y222D03*
+X1449Y222D03*
+X949Y472D03*
+X949Y72D03*
+G54D11*
+X749Y972D03*
+X599Y972D03*
+X349Y322D03*
+X349Y472D03*
+X349Y672D03*
+X349Y822D03*
+G54D10*
+X699Y122D03*
+X699Y322D03*
+G54D12*
+X699Y222D03*
+X949Y972D03*
+X749Y622D03*
+X1049Y222D03*
+X1249Y872D03*
+G54D13*
+X1149Y872D03*
+X1149Y522D03*
+G54D14*
+X952Y946D02*
+X1045Y249D01*
+G54D15*
+X776Y695D02*
+X721Y695D01*
+X721Y750D01*
+X776Y750D01*
+X776Y695D01*
+D02*
+X671Y150D02*
+X726Y150D01*
+X726Y95D01*
+X671Y95D01*
+X671Y150D01*
+D02*
+G04 End of Copper1*
+M02*

+ 46 - 0
tests/gerber_files/detector_drill.txt

@@ -0,0 +1,46 @@
+; NON-PLATED HOLES START AT T1
+; THROUGH (PLATED) HOLES START AT T100
+M48
+INCH
+T1C0.125984
+T100C0.031496
+T101C0.035000
+T102C0.059055
+%
+T1
+X001488Y010223
+X001488Y001223
+X016488Y001223
+X016488Y010223
+T100
+X009488Y009723
+X007488Y006223
+X012488Y008723
+X010488Y002223
+X006988Y002223
+T101
+X014488Y004223
+X006988Y003223
+X013488Y008723
+X011488Y008723
+X007488Y005223
+X014488Y003223
+X014488Y002223
+X011488Y005223
+X009488Y000723
+X011488Y004223
+X006988Y001223
+X009488Y004723
+X007488Y007223
+X011488Y003223
+X014488Y005223
+X011488Y002223
+T102
+X003488Y008223
+X003488Y004723
+X007488Y009723
+X003488Y006723
+X005988Y009723
+X003488Y003223
+T00
+M30

+ 180 - 0
tests/test_tcl_shell.py

@@ -0,0 +1,180 @@
+import sys
+import unittest
+from PyQt4 import QtGui
+from PyQt4.QtCore import QThread
+
+from FlatCAMApp import App
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon
+from ObjectUI import GerberObjectUI, GeometryObjectUI
+from time import sleep
+import os
+import tempfile
+
+class TclShellTest(unittest.TestCase):
+
+    gerber_files = 'tests/gerber_files'
+    copper_bottom_filename = 'detector_copper_bottom.gbr'
+    copper_top_filename = 'detector_copper_top.gbr'
+    cutout_filename = 'detector_contour.gbr'
+    excellon_filename = 'detector_drill.txt'
+    excellon_name = "excellon"
+    gerber_top_name = "top"
+    gerber_bottom_name = "bottom"
+    gerber_cutout_name = "cutout"
+    engraver_diameter = 0.3
+    cutout_diameter = 3
+    drill_diameter = 0.8
+
+    @classmethod
+    def setUpClass(self):
+
+        self.setup=True
+        self.app = QtGui.QApplication(sys.argv)
+        # Create App, keep app defaults (do not load
+        # user-defined defaults).
+        self.fc = App(user_defaults=False)
+        self.fc.ui.shell_dock.show()
+
+    @classmethod
+    def tearDownClass(self):
+        self.fc.tcl=None
+        self.app.closeAllWindows()
+        del self.fc
+        del self.app
+        pass
+
+    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('new')
+        units=self.fc.exec_command_test('get_sys units')
+        self.assertEquals(units, "IN")
+
+        self.fc.exec_command_test('set_sys units MM')
+        self.fc.exec_command_test('new')
+        units=self.fc.exec_command_test('get_sys units')
+        self.assertEquals(units, "MM")
+
+
+    def test_gerber_flow(self):
+
+        # open  gerber files top, bottom and cutout
+
+
+        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)))
+
+        self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name))
+        gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
+        self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber),
+                        "Expected FlatCAMGerber, instead, %s is %s" %
+                        (self.gerber_bottom_name, type(gerber_bottom_obj)))
+
+        self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
+        gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
+        self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber),
+                        "Expected FlatCAMGerber, instead, %s is %s" %
+                        (self.gerber_cutout_name, type(gerber_cutout_obj)))
+
+        # exteriors delete and join geometries for top layer
+        self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
+        self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
+        obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
+        self.assertTrue(isinstance(obj, FlatCAMGeometry),
+                        "Expected FlatCAMGeometry, instead, %s is %s" %
+                        (self.gerber_cutout_name + '_iso_exterior', type(obj)))
+
+        # mirror bottom gerbers
+        self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_bottom_name, self.gerber_cutout_name))
+        self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_cutout_name, self.gerber_cutout_name))
+
+        # exteriors delete and join geometries for bottom layer
+        self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.engraver_diameter, self.gerber_cutout_name + '_bottom_iso'))
+        self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
+        obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
+        self.assertTrue(isinstance(obj, FlatCAMGeometry),
+                        "Expected FlatCAMGeometry, instead, %s is %s" %
+                        (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
+
+        # at this stage we should have 5 objects
+        names = self.fc.collection.get_names()
+        self.assertEqual(len(names), 5,
+                         "Expected 5 objects, found %d" % len(names))
+
+        # isolate traces
+        self.fc.exec_command_test('isolate %s -dia %f' %  (self.gerber_top_name, self.engraver_diameter))
+        self.fc.exec_command_test('isolate %s -dia %f' %  (self.gerber_bottom_name, self.engraver_diameter))
+
+        # join isolated geometries for top and  bottom
+        self.fc.exec_command_test('join_geometries %s %s %s' %  (self.gerber_top_name + '_join_iso', self.gerber_top_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
+        self.fc.exec_command_test('join_geometries %s %s %s' %  (self.gerber_bottom_name + '_join_iso', self.gerber_bottom_name + '_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+
+        # at this stage we should have 9 objects
+        names = self.fc.collection.get_names()
+        self.assertEqual(len(names), 9,
+                         "Expected 9 objects, found %d" % len(names))
+
+        # clean unused isolations
+        self.fc.exec_command_test('delete %s' % (self.gerber_bottom_name + '_iso'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_top_name + '_iso'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso_exterior'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso_exterior'))
+
+        # at this stage we should have 5 objects again
+        names = self.fc.collection.get_names()
+        self.assertEqual(len(names), 5,
+                         "Expected 5 objects, found %d" % len(names))
+
+        # geocutout bottom test (it cuts  to same object)
+        self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.cutout_diameter, self.gerber_cutout_name + '_bottom_iso'))
+        self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+        self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
+        obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
+        self.assertTrue(isinstance(obj, FlatCAMGeometry),
+                        "Expected FlatCAMGeometry, instead, %s is %s" %
+                        (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
+        self.fc.exec_command_test('geocutout %s -dia %f -gapsize 0.3 -gaps 4' % (self.gerber_cutout_name + '_bottom_iso_exterior', self.cutout_diameter))
+
+        # at this stage we should have 6 objects
+        names = self.fc.collection.get_names()
+        self.assertEqual(len(names), 6,
+                         "Expected 6 objects, found %d" % len(names))
+
+        # 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):
+
+        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))
+        excellon_obj = self.fc.collection.get_by_name(self.excellon_name)
+        self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),
+                        "Expected FlatCAMExcellon, instead, %s is %s" %
+                        (self.excellon_name, type(excellon_obj)))
+
+        # mirror bottom excellon
+        self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.excellon_name, self.gerber_cutout_name))
+
+        # TODO: tests for tcl