Browse Source

Merged in sopak/flatcam/tcl-commands (pull request #37)

TCL commands redesign
jpcgt 9 years ago
parent
commit
d730335fed

+ 5 - 0
FlatCAM.py

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

+ 320 - 232
FlatCAMApp.py

@@ -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)
@@ -528,8 +531,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 +562,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 +675,77 @@ 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
+        :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
         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)
         self.tcl.eval('return -code error "%s"' % text)
-        raise Exception(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 +1079,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 +1092,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 +1540,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 +1804,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 +1831,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 +2470,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 +2833,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 +2898,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 obj is None:
+                    self.raise_tcl_error("Object not found: %s" % name)
 
 
-            if not isinstance(obj, FlatCAMExcellon):
-                self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name,  type(obj)))
+                if not isinstance(obj, FlatCAMExcellon):
+                    self.raise_tcl_error('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj)))
 
 
-            try:
-                success, msg = obj.generate_milling(**kwa)
-            except Exception as e:
-                self.raiseTclError("Operation failed: %s" % str(e))
+                try:
+                    success, msg = obj.generate_milling(**kwa)
+                except Exception as e:
+                    self.raise_tcl_error("Operation failed: %s" % str(e))
 
 
-            if not success:
-                self.raiseTclError(msg)
+                if not success:
+                    self.raise_tcl_error(msg)
 
 
-            return 'Ok'
+            except Exception as unknown:
+                self.raise_tcl_unknown_error(unknown)
 
 
         def exteriors(name=None, *args):
         def exteriors(name=None, *args):
             '''
             '''
@@ -2872,46 +2951,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 +3002,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 +3062,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 +3475,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 +3861,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'])

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

+ 21 - 0
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.

BIN
camlib.pyc


BIN
descartes/__init__.pyc


BIN
descartes/patch.pyc


+ 358 - 0
tclCommands/TclCommand.py

@@ -0,0 +1,358 @@
+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))
+
+        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 is not 'timeout':
+                self.raise_tcl_error('Unknown parameter: %s' % key)
+            try:
+                named_args[key] = self.option_types[key](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 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:
+            self.log.error("TCL command '%s' failed." % str(self))
+            self.app.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
+    """
+
+    # default  timeout for operation is  10 sec, but it can be much more
+    default_timeout = 10000
+
+    output = None
+
+    def execute_call(self, args, unnamed_args):
+
+        try:
+            self.output = self.execute(args, unnamed_args)
+        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=10000):
+            """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:
+                self.raise_tcl_error(str(ex[0]))
+
+            if status['timed_out']:
+                self.app.raise_tcl_unknown_error('Operation timed out!')
+
+        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.default_timeout
+
+            # set detail for processing, it will be there until next open or close
+            self.app.shell.open_proccessing(self.get_current_command())
+
+            self.output = None
+
+            def handle_finished(obj):
+                self.app.shell_command_finished.disconnect(handle_finished)
+                # TODO: handle output
+                pass
+
+            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:
+            self.log.error("TCL command '%s' failed." % str(self))
+            self.app.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()

+ 81 - 0
tclCommands/TclCommandCncjob.py

@@ -0,0 +1,81 @@
+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.'),
+            ('timeout', 'Max wait for job timeout before error.')
+        ]),
+        '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)

+ 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
+import tclCommands.TclCommandAddPolygon
+import tclCommands.TclCommandAddPolyline
+import tclCommands.TclCommandCncjob
+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