Procházet zdrojové kódy

Merged jpcgt/flatcam into master

Kamil Sopko před 9 roky
rodič
revize
dcd3c4c793

+ 11 - 2
FlatCAM.py

@@ -2,14 +2,23 @@ import sys
 from PyQt4 import QtGui
 from PyQt4 import QtGui
 from FlatCAMApp import App
 from FlatCAMApp import App
 
 
+
 def debug_trace():
 def debug_trace():
-    '''Set a tracepoint in the Python debugger that works with Qt'''
+    """
+    Set a tracepoint in the Python debugger that works with Qt
+    :return: None
+    """
     from PyQt4.QtCore import pyqtRemoveInputHook
     from PyQt4.QtCore import pyqtRemoveInputHook
     #from pdb import set_trace
     #from pdb import set_trace
     pyqtRemoveInputHook()
     pyqtRemoveInputHook()
     #set_trace()
     #set_trace()
 
 
 debug_trace()
 debug_trace()
+
+# All X11 calling should be thread safe otherwise we have strange issues
+# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
+# NOTE: Never talk to the GUI from threads! This is why I commented the above.
+
 app = QtGui.QApplication(sys.argv)
 app = QtGui.QApplication(sys.argv)
 fc = App()
 fc = App()
-sys.exit(app.exec_())
+sys.exit(app.exec_())

+ 360 - 234
FlatCAMApp.py

@@ -1,4 +1,4 @@
-import sys
+import sys, traceback
 import urllib
 import urllib
 import getopt
 import getopt
 import random
 import random
@@ -27,7 +27,7 @@ from FlatCAMDraw import FlatCAMDraw
 from FlatCAMProcess import *
 from FlatCAMProcess import *
 from MeasurementTool import Measurement
 from MeasurementTool import Measurement
 from DblSidedTool import DblSidedTool
 from DblSidedTool import DblSidedTool
-
+import tclCommands
 
 
 ########################################
 ########################################
 ##                App                 ##
 ##                App                 ##
@@ -107,6 +107,9 @@ class App(QtCore.QObject):
 
 
     message = QtCore.pyqtSignal(str, str, str)
     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
     # Emitted when an unhandled exception happens
     # in the worker task.
     # in the worker task.
     thread_exception = QtCore.pyqtSignal(object)
     thread_exception = QtCore.pyqtSignal(object)
@@ -228,6 +231,7 @@ class App(QtCore.QObject):
             "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry,
             "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry,
             "excellon_spindlespeed": self.defaults_form.excellon_group.spindlespeed_entry,
             "excellon_spindlespeed": self.defaults_form.excellon_group.spindlespeed_entry,
             "excellon_toolchangez": self.defaults_form.excellon_group.toolchangez_entry,
             "excellon_toolchangez": self.defaults_form.excellon_group.toolchangez_entry,
+            "excellon_tooldia": self.defaults_form.excellon_group.tooldia_entry,
             "geometry_plot": self.defaults_form.geometry_group.plot_cb,
             "geometry_plot": self.defaults_form.geometry_group.plot_cb,
             "geometry_cutz": self.defaults_form.geometry_group.cutz_entry,
             "geometry_cutz": self.defaults_form.geometry_group.cutz_entry,
             "geometry_travelz": self.defaults_form.geometry_group.travelz_entry,
             "geometry_travelz": self.defaults_form.geometry_group.travelz_entry,
@@ -283,6 +287,8 @@ class App(QtCore.QObject):
             "cncjob_tooldia": 0.016,
             "cncjob_tooldia": 0.016,
             "cncjob_prepend": "",
             "cncjob_prepend": "",
             "cncjob_append": "",
             "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
             # Persistence
             "last_folder": None,
             "last_folder": None,
@@ -355,6 +361,7 @@ class App(QtCore.QObject):
             "excellon_feedrate": self.options_form.excellon_group.feedrate_entry,
             "excellon_feedrate": self.options_form.excellon_group.feedrate_entry,
             "excellon_spindlespeed": self.options_form.excellon_group.spindlespeed_entry,
             "excellon_spindlespeed": self.options_form.excellon_group.spindlespeed_entry,
             "excellon_toolchangez": self.options_form.excellon_group.toolchangez_entry,
             "excellon_toolchangez": self.options_form.excellon_group.toolchangez_entry,
+            "excellon_tooldia": self.options_form.excellon_group.tooldia_entry,
             "geometry_plot": self.options_form.geometry_group.plot_cb,
             "geometry_plot": self.options_form.geometry_group.plot_cb,
             "geometry_cutz": self.options_form.geometry_group.cutz_entry,
             "geometry_cutz": self.options_form.geometry_group.cutz_entry,
             "geometry_travelz": self.options_form.geometry_group.travelz_entry,
             "geometry_travelz": self.options_form.geometry_group.travelz_entry,
@@ -528,8 +535,8 @@ class App(QtCore.QObject):
         self.shell.resize(*self.defaults["shell_shape"])
         self.shell.resize(*self.defaults["shell_shape"])
         self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version)
         self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version)
         self.shell.append_output("Type help to get started.\n\n")
         self.shell.append_output("Type help to get started.\n\n")
-        self.tcl = Tkinter.Tcl()
-        self.setup_shell()
+
+        self.init_tcl()
 
 
         self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell")
         self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell")
         self.ui.shell_dock.setWidget(self.shell)
         self.ui.shell_dock.setWidget(self.shell)
@@ -559,6 +566,17 @@ class App(QtCore.QObject):
 
 
         App.log.debug("END of constructor. Releasing control.")
         App.log.debug("END of constructor. Releasing control.")
 
 
+    def init_tcl(self):
+        if hasattr(self,'tcl'):
+            # self.tcl = None
+            # TODO  we need  to clean  non default variables and procedures here
+            # new object cannot be used here as it  will not remember values created for next passes,
+            # because tcl  was execudted in old instance of TCL
+            pass
+        else:
+            self.tcl = Tkinter.Tcl()
+            self.setup_shell()
+
     def defaults_read_form(self):
     def defaults_read_form(self):
         for option in self.defaults_form_fields:
         for option in self.defaults_form_fields:
             self.defaults[option] = self.defaults_form_fields[option].get_value()
             self.defaults[option] = self.defaults_form_fields[option].get_value()
@@ -661,36 +679,111 @@ class App(QtCore.QObject):
         else:
         else:
             self.defaults['stats'][resource] = 1
             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
         this method  pass exception from python into TCL as error, so we get stacktrace and reason
         :param text: text of error
         :param text: text of error
         :return: raise exception
         :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):
     def exec_command(self, text):
         """
         """
         Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
         Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+        Also handles execution in separated threads
 
 
-        :param text: Input command
-        :return: None
+        :param text:
+        :return: output if there was any
         """
         """
+
         self.report_usage('exec_command')
         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)
         text = str(text)
 
 
         try:
         try:
+            self.shell.open_proccessing()
             result = self.tcl.eval(str(text))
             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:
         except Tkinter.TclError, e:
             #this will display more precise answer if something in  TCL shell fail
             #this will display more precise answer if something in  TCL shell fail
             result = self.tcl.eval("set errorInfo")
             result = self.tcl.eval("set errorInfo")
             self.log.error("Exec command Exception: %s" % (result + '\n'))
             self.log.error("Exec command Exception: %s" % (result + '\n'))
             self.shell.append_error('ERROR: ' + 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.
         Code below is unsused. Saved for later.
@@ -1024,6 +1117,7 @@ class App(QtCore.QObject):
         toggle shell if is  visible close it if  closed open it
         toggle shell if is  visible close it if  closed open it
         :return:
         :return:
         """
         """
+
         if self.ui.shell_dock.isVisible():
         if self.ui.shell_dock.isVisible():
             self.ui.shell_dock.hide()
             self.ui.shell_dock.hide()
         else:
         else:
@@ -1036,6 +1130,7 @@ class App(QtCore.QObject):
 
 
         :return: None
         :return: None
         """
         """
+
         objs = self.collection.get_selected()
         objs = self.collection.get_selected()
 
 
         def initialize(obj, app):
         def initialize(obj, app):
@@ -1483,6 +1578,9 @@ class App(QtCore.QObject):
 
 
         self.plotcanvas.clear()
         self.plotcanvas.clear()
 
 
+        # tcl needs to be reinitialized, otherwise  old shell variables etc  remains
+        self.init_tcl()
+
         self.collection.delete_all()
         self.collection.delete_all()
 
 
         self.setup_component_editor()
         self.setup_component_editor()
@@ -1744,6 +1842,7 @@ class App(QtCore.QObject):
         :param outname:
         :param outname:
         :return:
         :return:
         """
         """
+
         self.log.debug("export_svg()")
         self.log.debug("export_svg()")
 
 
         try:
         try:
@@ -1770,7 +1869,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 = '<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 += 'width="' + svgwidth + uom + '" '
             svg_header += 'height="' + svgheight + 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_header += '<g transform="scale(1,-1)">'
             svg_footer = '</g> </svg>'
             svg_footer = '</g> </svg>'
             svg_elem = svg_header + exported_svg + svg_footer
             svg_elem = svg_header + exported_svg + svg_footer
@@ -2409,85 +2508,96 @@ class App(QtCore.QObject):
             return 'Ok'
             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
             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):
         def mirror(name, *args):
             a, kwa = h(*args)
             a, kwa = h(*args)
@@ -2761,59 +2871,63 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :param args: array of arguments
             :return: "Ok" if completed without errors
             :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:
                 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):
         def millholes(name=None, *args):
             '''
             '''
@@ -2822,48 +2936,51 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :param args: array of arguments
             :return: "Ok" if completed without errors
             :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):
         def exteriors(name=None, *args):
             '''
             '''
@@ -2872,46 +2989,49 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :param args: array of arguments
             :return: "Ok" if completed without errors
             :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):
         def interiors(name=None, *args):
             '''
             '''
@@ -2920,46 +3040,49 @@ class App(QtCore.QObject):
             :param args: array of arguments
             :param args: array of arguments
             :return: "Ok" if completed without errors
             :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):
         def isolate(name=None, *args):
             '''
             '''
@@ -2977,29 +3100,29 @@ class App(QtCore.QObject):
 
 
             for key in kwa:
             for key in kwa:
                 if key not in types:
                 if key not in types:
-                    self.raiseTclError('Unknown parameter: %s' % key)
+                    self.raise_tcl_error('Unknown parameter: %s' % key)
                 try:
                 try:
                     kwa[key] = types[key](kwa[key])
                     kwa[key] = types[key](kwa[key])
                 except Exception, e:
                 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:
             try:
                 obj = self.collection.get_by_name(str(name))
                 obj = self.collection.get_by_name(str(name))
             except:
             except:
-                self.raiseTclError("Could not retrieve object: %s" % name)
+                self.raise_tcl_error("Could not retrieve object: %s" % name)
 
 
             if obj is None:
             if obj is None:
-                self.raiseTclError("Object not found: %s" % name)
+                self.raise_tcl_error("Object not found: %s" % name)
 
 
             assert isinstance(obj, FlatCAMGerber), \
             assert isinstance(obj, FlatCAMGerber), \
                 "Expected a FlatCAMGerber, got %s" % type(obj)
                 "Expected a FlatCAMGerber, got %s" % type(obj)
 
 
             if not isinstance(obj, FlatCAMGerber):
             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:
             try:
                 obj.isolate(**kwa)
                 obj.isolate(**kwa)
             except Exception, e:
             except Exception, e:
-                self.raiseTclError("Operation failed: %s" % str(e))
+                self.raise_tcl_error("Operation failed: %s" % str(e))
 
 
             return 'Ok'
             return 'Ok'
 
 
@@ -3390,11 +3513,11 @@ class App(QtCore.QObject):
             Test it like this:
             Test it like this:
             if name is None:
             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.
             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.
             which is catched in exec_command and displayed in TCL shell console with red background.
             Error in console is displayed  with TCL  trace.
             Error in console is displayed  with TCL  trace.
 
 
@@ -3776,6 +3899,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
         # Add commands to the tcl interpreter
         for cmd in commands:
         for cmd in commands:
             self.tcl.createcommand(cmd, commands[cmd]['fcn'])
             self.tcl.createcommand(cmd, commands[cmd]['fcn'])

+ 17 - 0
FlatCAMGUI.py

@@ -607,6 +607,23 @@ class ExcellonOptionsGroupUI(OptionsGroupUI):
         self.spindlespeed_entry = IntEntry(allow_empty=True)
         self.spindlespeed_entry = IntEntry(allow_empty=True)
         grid1.addWidget(self.spindlespeed_entry, 4, 1)
         grid1.addWidget(self.spindlespeed_entry, 4, 1)
 
 
+        #### Milling Holes ####
+        self.mill_hole_label = QtGui.QLabel('<b>Mill Holes</b>')
+        self.mill_hole_label.setToolTip(
+            "Create Geometry for milling holes."
+        )
+        self.layout.addWidget(self.mill_hole_label)
+
+        grid1 = QtGui.QGridLayout()
+        self.layout.addLayout(grid1)
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid1.addWidget(tdlabel, 0, 0)
+        self.tooldia_entry = LengthEntry()
+        grid1.addWidget(self.tooldia_entry, 0, 1)
+
 
 
 class GeometryOptionsGroupUI(OptionsGroupUI):
 class GeometryOptionsGroupUI(OptionsGroupUI):
     def __init__(self, parent=None):
     def __init__(self, parent=None):

+ 20 - 11
FlatCAMObj.py

@@ -1040,6 +1040,10 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
 
         self.app.inform.emit("Saved to: " + filename)
         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):
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
         if self.muted_ui:
             return
             return
@@ -1243,7 +1247,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                        outname=None,
                        outname=None,
                        spindlespeed=None,
                        spindlespeed=None,
                        multidepth=None,
                        multidepth=None,
-                       depthperpass=None):
+                       depthperpass=None,
+                       use_thread=True):
         """
         """
         Creates a CNCJob out of this Geometry object. The actual
         Creates a CNCJob out of this Geometry object. The actual
         work is done by the target FlatCAMCNCjob object's
         work is done by the target FlatCAMCNCjob object's
@@ -1304,18 +1309,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
             app_obj.progress.emit(80)
             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
     def on_plot_cb_click(self, *args):  # TODO: args not needed
         if self.muted_ui:
         if self.muted_ui:

+ 23 - 6
FlatCAMWorker.py

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

+ 49 - 21
camlib.py

@@ -136,6 +136,27 @@ class Geometry(object):
             log.error("Failed to run union on polygons.")
             log.error("Failed to run union on polygons.")
             raise
             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):
     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.
         Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
@@ -2756,12 +2777,14 @@ class CNCjob(Geometry):
         # so we actually are sorting the tools by diameter
         # so we actually are sorting the tools by diameter
         sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
         sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
         if tools == "all":
         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))
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
         else:
         else:
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
             selected_tools = filter(lambda i: i in selected_tools, selected_tools)
             selected_tools = filter(lambda i: i in selected_tools, selected_tools)
-            tools = [i for i,j in sorted_tools for k in selected_tools if i == k]   # create a sorted list of selected tools from the sorted_tools list
+
+            # Create a sorted list of selected tools from the sorted_tools list
+            tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
             log.debug("Tools selected and sorted are: %s" % str(tools)) 
             log.debug("Tools selected and sorted are: %s" % str(tools)) 
 
 
         # Points (Group by tool)
         # Points (Group by tool)
@@ -2779,7 +2802,8 @@ class CNCjob(Geometry):
         # Basic G-Code macros
         # Basic G-Code macros
         t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n"
         t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n"
         down = "G01 Z%.4f\n" % self.z_cut
         down = "G01 Z%.4f\n" % self.z_cut
-        up = "G01 Z%.4f\n" % self.z_move
+        up = "G00 Z%.4f\n" % self.z_move
+        up_to_zero = "G01 Z0\n"
 
 
         # Initialization
         # Initialization
         gcode = self.unitcode[self.units.upper()] + "\n"
         gcode = self.unitcode[self.units.upper()] + "\n"
@@ -2789,7 +2813,8 @@ class CNCjob(Geometry):
         gcode += "G00 Z%.4f\n" % self.z_move  # Move to travel height
         gcode += "G00 Z%.4f\n" % self.z_move  # Move to travel height
 
 
         if self.spindlespeed is not None:
         if self.spindlespeed is not None:
-            gcode += "M03 S%d\n" % int(self.spindlespeed)  # Spindle start with configured speed
+            # Spindle start with configured speed
+            gcode += "M03 S%d\n" % int(self.spindlespeed)
         else:
         else:
             gcode += "M03\n"  # Spindle start
             gcode += "M03\n"  # Spindle start
 
 
@@ -2797,24 +2822,27 @@ class CNCjob(Geometry):
 
 
         for tool in tools:
         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 has points.
+            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:
+                        # Spindle start with configured speed
+                        gcode += "M03 S%d\n" % int(self.spindlespeed)
+                    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_to_zero + up
 
 
         gcode += t % (0, 0)
         gcode += t % (0, 0)
         gcode += "M05\n"  # Spindle stop
         gcode += "M05\n"  # Spindle stop

binární
camlib.pyc


binární
descartes/__init__.pyc


binární
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
 import cgi
-
-from PyQt4.QtCore import pyqtSignal
+from PyQt4.QtCore import pyqtSignal, Qt
 from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
 from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
                         QSizePolicy, QTextCursor, QTextEdit, \
                         QSizePolicy, QTextCursor, QTextEdit, \
                         QVBoxLayout, QWidget
                         QVBoxLayout, QWidget
@@ -83,7 +82,6 @@ class _ExpandableTextEdit(QTextEdit):
         # Paste only plain text.
         # Paste only plain text.
         self.insertPlainText(mime_data.text())
         self.insertPlainText(mime_data.text())
 
 
-
 class TermWidget(QWidget):
 class TermWidget(QWidget):
     """
     """
     Widget wich represents terminal. It only displays text and allows to enter text.
     Widget wich represents terminal. It only displays text and allows to enter text.
@@ -118,6 +116,34 @@ class TermWidget(QWidget):
 
 
         self._edit.setFocus()
         self._edit.setFocus()
 
 
+    def open_proccessing(self, detail=None):
+        """
+        Open processing and disable using shell commands  again until all commands are finished
+
+        :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):
     def _append_to_browser(self, style, text):
         """
         """
         Convert text to HTML for inserting it to browser
         Convert text to HTML for inserting it to browser
@@ -225,4 +251,3 @@ class TermWidget(QWidget):
             self._historyIndex -= 1
             self._historyIndex -= 1
             self._edit.setPlainText(self._history[self._historyIndex])
             self._edit.setPlainText(self._history[self._historyIndex])
             self._edit.moveCursor(QTextCursor.End)
             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