diff --git a/FlatCAM.py b/FlatCAM.py index 1c1b1f7f..1cb60c9f 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -1,5 +1,6 @@ import sys from PyQt4 import QtGui +from PyQt4 import QtCore from FlatCAMApp import App def debug_trace(): @@ -10,6 +11,10 @@ def debug_trace(): #set_trace() debug_trace() + +# all X11 calling should be thread safe otherwise we have strange issues +QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) + app = QtGui.QApplication(sys.argv) fc = App() sys.exit(app.exec_()) \ No newline at end of file diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c510b742..872a6e4f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1,4 +1,4 @@ -import sys +import sys, traceback import urllib import getopt import random @@ -27,7 +27,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +import tclCommands ######################################## ## App ## @@ -107,6 +107,9 @@ class App(QtCore.QObject): message = QtCore.pyqtSignal(str, str, str) + # Emmited when shell command is finished(one command only) + shell_command_finished = QtCore.pyqtSignal(object) + # Emitted when an unhandled exception happens # in the worker task. thread_exception = QtCore.pyqtSignal(object) @@ -283,6 +286,8 @@ class App(QtCore.QObject): "cncjob_tooldia": 0.016, "cncjob_prepend": "", "cncjob_append": "", + "background_timeout": 300000, #default value is 5 minutes + "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future). # Persistence "last_folder": None, @@ -528,8 +533,8 @@ class App(QtCore.QObject): self.shell.resize(*self.defaults["shell_shape"]) self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version) self.shell.append_output("Type help to get started.\n\n") - self.tcl = Tkinter.Tcl() - self.setup_shell() + + self.init_tcl() self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") self.ui.shell_dock.setWidget(self.shell) @@ -559,6 +564,17 @@ class App(QtCore.QObject): App.log.debug("END of constructor. Releasing control.") + def init_tcl(self): + if hasattr(self,'tcl'): + # self.tcl = None + # TODO we need to clean non default variables and procedures here + # new object cannot be used here as it will not remember values created for next passes, + # because tcl was execudted in old instance of TCL + pass + else: + self.tcl = Tkinter.Tcl() + self.setup_shell() + def defaults_read_form(self): for option in self.defaults_form_fields: self.defaults[option] = self.defaults_form_fields[option].get_value() @@ -661,36 +677,111 @@ class App(QtCore.QObject): else: self.defaults['stats'][resource] = 1 - def raiseTclError(self, text): + class TclErrorException(Exception): + """ + this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command + """ + pass + + def raise_tcl_unknown_error(self, unknownException): + """ + raise Exception if is different type than TclErrorException + this is here mainly to show unknown errors inside TCL shell console + :param unknownException: + :return: + """ + + if not isinstance(unknownException, self.TclErrorException): + self.raise_tcl_error("Unknown error: %s" % str(unknownException)) + else: + raise unknownException + + def display_tcl_error(self, error, error_info=None): + """ + escape bracket [ with \ otherwise there is error + "ERROR: missing close-bracket" instead of real error + :param error: it may be text or exception + :return: None + """ + + if isinstance(error, Exception): + + exc_type, exc_value, exc_traceback = error_info + if not isinstance(error, self.TclErrorException): + show_trace = 1 + else: + show_trace = int(self.defaults['verbose_error_level']) + + if show_trace > 0: + trc=traceback.format_list(traceback.extract_tb(exc_traceback)) + trc_formated=[] + for a in reversed(trc): + trc_formated.append(a.replace(" ", " > ").replace("\n","")) + text="%s\nPython traceback: %s\n%s" % (exc_value, + exc_type, + "\n".join(trc_formated)) + + else: + text="%s" % error + else: + text=error + + text = text.replace('[', '\\[').replace('"','\\"') + + self.tcl.eval('return -code error "%s"' % text) + + def raise_tcl_error(self, text): """ this method pass exception from python into TCL as error, so we get stacktrace and reason :param text: text of error :return: raise exception """ - self.tcl.eval('return -code error "%s"' % text) - raise Exception(text) + + self.display_tcl_error(text) + raise self.TclErrorException(text) def exec_command(self, text): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + Also handles execution in separated threads + + :param text: + :return: output if there was any + """ + + self.report_usage('exec_command') + + result = self.exec_command_test(text, False) + return result + + def exec_command_test(self, text, reraise=True): + """ + Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. :param text: Input command - :return: None + :param reraise: raise exception and not hide it, used mainly in unittests + :return: output if there was any """ - self.report_usage('exec_command') text = str(text) try: + self.shell.open_proccessing() result = self.tcl.eval(str(text)) - self.shell.append_output(result + '\n') + if result!='None': + self.shell.append_output(result + '\n') except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') - #show error in console and just return - return + #show error in console and just return or in test raise exception + if reraise: + raise e + finally: + self.shell.close_proccessing() + pass + return result """ Code below is unsused. Saved for later. @@ -1024,6 +1115,7 @@ class App(QtCore.QObject): toggle shell if is visible close it if closed open it :return: """ + if self.ui.shell_dock.isVisible(): self.ui.shell_dock.hide() else: @@ -1036,6 +1128,7 @@ class App(QtCore.QObject): :return: None """ + objs = self.collection.get_selected() def initialize(obj, app): @@ -1483,6 +1576,9 @@ class App(QtCore.QObject): self.plotcanvas.clear() + # tcl needs to be reinitialized, otherwise old shell variables etc remains + self.init_tcl() + self.collection.delete_all() self.setup_component_editor() @@ -1744,6 +1840,7 @@ class App(QtCore.QObject): :param outname: :return: """ + self.log.debug("export_svg()") try: @@ -1770,7 +1867,7 @@ class App(QtCore.QObject): svg_header = '' + svg_header += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">' svg_header += '' svg_footer = ' ' svg_elem = svg_header + exported_svg + svg_footer @@ -2409,85 +2506,96 @@ class App(QtCore.QObject): return 'Ok' - def geocutout(name, *args): - """ + def geocutout(name=None, *args): + ''' + TCL shell command - see help section + Subtract gaps from geometry, this will not create new object - :param name: - :param args: - :return: - """ - a, kwa = h(*args) - types = {'dia': float, - 'gapsize': float, - 'gaps': str} - - # 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]) + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' try: - obj = self.collection.get_by_name(str(name)) - except: - return "Could not retrieve object: %s" % name + a, kwa = h(*args) + types = {'dia': float, + 'gapsize': float, + 'gaps': str} - # 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' + # 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 + + 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]))) + + try: + obj = self.collection.get_by_name(str(name)) + except: + self.raise_tcl_error("Could not retrieve object: %s" % name) + + # Get min and max data for each object as we just cut rectangles across X or Y + xmin, ymin, xmax, ymax = obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + lenghtx = (xmax - xmin) + lenghty = (ymax - ymin) + gapsize = kwa['gapsize'] + kwa['dia'] / 2 + + if kwa['gaps'] == '8' or kwa['gaps']=='2lr': + + subtract_rectangle(name, + xmin - gapsize, + py - gapsize + lenghty / 4, + xmax + gapsize, + py + gapsize + lenghty / 4) + subtract_rectangle(name, + xmin-gapsize, + py - gapsize - lenghty / 4, + xmax + gapsize, + py + gapsize - lenghty / 4) + + if kwa['gaps'] == '8' or kwa['gaps']=='2tb': + subtract_rectangle(name, + px - gapsize + lenghtx / 4, + ymin-gapsize, + px + gapsize + lenghtx / 4, + ymax + gapsize) + subtract_rectangle(name, + px - gapsize - lenghtx / 4, + ymin - gapsize, + px + gapsize - lenghtx / 4, + ymax + gapsize) + + if kwa['gaps'] == '4' or kwa['gaps']=='lr': + subtract_rectangle(name, + xmin - gapsize, + py - gapsize, + xmax + gapsize, + py + gapsize) + + if kwa['gaps'] == '4' or kwa['gaps']=='tb': + subtract_rectangle(name, + px - gapsize, + ymin - gapsize, + px + gapsize, + ymax + gapsize) + + except Exception as unknown: + self.raise_tcl_unknown_error(unknown) def mirror(name, *args): a, kwa = h(*args) @@ -2761,59 +2869,63 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'tools': str, - 'outname': str, - 'drillz': float, - 'travelz': float, - 'feedrate': float, - 'spindlespeed': int, - 'toolchange': int - } - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'tools': str, + 'outname': str, + 'drillz': float, + 'travelz': float, + 'feedrate': float, + 'spindlespeed': int, + 'toolchange': int + } + + if name is None: + self.raise_tcl_error('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raise_tcl_error('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) + 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.raise_tcl_error('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) + except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + self.raise_tcl_error("Operation failed: %s" % str(e)) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + except Exception as unknown: + self.raise_tcl_unknown_error(unknown) - if obj is None: - self.raiseTclError('Object not found: %s' % name) - - 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) - - except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) - - return 'Ok' def millholes(name=None, *args): ''' @@ -2822,48 +2934,51 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'tooldia': float, - 'tools': str, - 'outname': str} - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'tooldia': float, + 'tools': str, + 'outname': str} + + 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, types[key])) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + 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)) - 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)) + try: + 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 mill drilled, got %s %s.' % (name, type(obj))) - if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('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.raise_tcl_error("Operation failed: %s" % str(e)) - try: - success, msg = obj.generate_milling(**kwa) - except Exception as e: - self.raiseTclError("Operation failed: %s" % str(e)) + if not success: + self.raise_tcl_error(msg) - if not success: - self.raiseTclError(msg) - - return 'Ok' + except Exception as unknown: + self.raise_tcl_unknown_error(unknown) def exteriors(name=None, *args): ''' @@ -2872,46 +2987,49 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'outname': str} - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'outname': str} + + 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, types[key])) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, 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, Geometry): + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors - def geo_init(geo_obj, app_obj): - geo_obj.solid_geometry = obj_exteriors + if 'outname' in kwa: + outname = kwa['outname'] + else: + outname = name + ".exteriors" - if 'outname' in kwa: - outname = kwa['outname'] - else: - outname = name + ".exteriors" + 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)) - try: - obj_exteriors = obj.get_exteriors() - self.new_object('geometry', outname, geo_init) - except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) - - return 'Ok' + except Exception as unknown: + self.raise_tcl_unknown_error(unknown) def interiors(name=None, *args): ''' @@ -2920,46 +3038,49 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'outname': str} - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + try: + a, kwa = h(*args) + types = {'outname': str} + + 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 name is None: + self.raise_tcl_error('Argument name is missing.') + try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + obj = self.collection.get_by_name(str(name)) + except: + self.raise_tcl_error("Could not retrieve object: %s" % name) - if name is None: - self.raiseTclError('Argument name is missing.') + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + if not isinstance(obj, Geometry): + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_interiors - if not isinstance(obj, Geometry): - self.raiseTclError('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.raise_tcl_error("Failed: %s" % str(e)) - if 'outname' in kwa: - outname = kwa['outname'] - else: - outname = name + ".interiors" - - try: - obj_interiors = obj.get_interiors() - self.new_object('geometry', outname, geo_init) - except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) - - return 'Ok' + except Exception as unknown: + self.raise_tcl_unknown_error(unknown) def isolate(name=None, *args): ''' @@ -2977,29 +3098,29 @@ class App(QtCore.QObject): for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key])) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) assert isinstance(obj, FlatCAMGerber), \ "Expected a FlatCAMGerber, got %s" % type(obj) if not isinstance(obj, FlatCAMGerber): - self.raiseTclError('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) try: obj.isolate(**kwa) except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) return 'Ok' @@ -3390,11 +3511,11 @@ class App(QtCore.QObject): Test it like this: if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') - When error ocurre, always use raiseTclError, never return "sometext" on error, + When error ocurre, always use raise_tcl_error, never return "sometext" on error, otherwise we will miss it and processing will silently continue. - Method raiseTclError pass error into TCL interpreter, then raise python exception, + Method raise_tcl_error pass error into TCL interpreter, then raise python exception, which is catched in exec_command and displayed in TCL shell console with red background. Error in console is displayed with TCL trace. @@ -3776,6 +3897,9 @@ class App(QtCore.QObject): } } + #import/overwrite tcl commands as objects of TclCommand descendants + tclCommands.register_all_commands(self, commands) + # Add commands to the tcl interpreter for cmd in commands: self.tcl.createcommand(cmd, commands[cmd]['fcn']) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index b8315fc5..b4756920 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -1040,6 +1040,10 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.app.inform.emit("Saved to: " + filename) + def get_gcode(self, preamble='', postamble=''): + #we need this to beable get_gcode separatelly for shell command export_code + return preamble + '\n' + self.gcode + "\n" + postamble + def on_plot_cb_click(self, *args): if self.muted_ui: return @@ -1243,7 +1247,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): outname=None, spindlespeed=None, multidepth=None, - depthperpass=None): + depthperpass=None, + use_thread=True): """ Creates a CNCJob out of this Geometry object. The actual work is done by the target FlatCAMCNCjob object's @@ -1304,18 +1309,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(80) - # To be run in separate thread - def job_thread(app_obj): - with self.app.proc_container.new("Generating CNC Job."): - app_obj.new_object("cncjob", outname, job_init) - app_obj.inform.emit("CNCjob created: %s" % outname) - app_obj.progress.emit(100) - # Create a promise with the name - self.app.collection.promise(outname) + if use_thread: + # To be run in separate thread + def job_thread(app_obj): + with self.app.proc_container.new("Generating CNC Job."): + app_obj.new_object("cncjob", outname, job_init) + app_obj.inform.emit("CNCjob created: %s" % outname) + app_obj.progress.emit(100) - # Send to worker - self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + # Create a promise with the name + self.app.collection.promise(outname) + + # Send to worker + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + else: + self.app.new_object("cncjob", outname, job_init) def on_plot_cb_click(self, *args): # TODO: args not needed if self.muted_ui: diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index e97d9d11..8c13f4b1 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -1,5 +1,4 @@ from PyQt4 import QtCore -#import FlatCAMApp class Worker(QtCore.QObject): @@ -8,15 +7,34 @@ class Worker(QtCore.QObject): in a single independent thread. """ + # avoid multiple tests for debug availability + pydevd_failed = False + def __init__(self, app, name=None): super(Worker, self).__init__() self.app = app self.name = name + def allow_debug(self): + """ + allow debuging/breakpoints in this threads + should work from PyCharm and PyDev + :return: + """ + + if not self.pydevd_failed: + try: + import pydevd + pydevd.settrace(suspend=False, trace_only_current_thread=True) + except ImportError: + self.pydevd_failed=True + def run(self): self.app.log.debug("Worker Started!") + self.allow_debug() + # Tasks are queued in the event listener. self.app.worker_task.connect(self.do_worker_task) @@ -24,10 +42,10 @@ class Worker(QtCore.QObject): self.app.log.debug("Running task: %s" % str(task)) - # 'worker_name' property of task allows to target - # specific worker. + self.allow_debug() + if ('worker_name' in task and task['worker_name'] == self.name) or \ - ('worker_name' not in task and self.name is None): + ('worker_name' not in task and self.name is None): try: task['fcn'](*task['params']) @@ -37,5 +55,4 @@ class Worker(QtCore.QObject): return - # FlatCAMApp.App.log.debug("Task ignored.") - self.app.log.debug("Task ignored.") \ No newline at end of file + self.app.log.debug("Task ignored.") diff --git a/camlib.py b/camlib.py index 23bf1bcf..7c0cd11a 100644 --- a/camlib.py +++ b/camlib.py @@ -136,6 +136,27 @@ class Geometry(object): log.error("Failed to run union on polygons.") raise + def add_polyline(self, points): + """ + Adds a polyline to the object (by union) + + :param points: The vertices of the polyline. + :return: None + """ + if self.solid_geometry is None: + self.solid_geometry = [] + + if type(self.solid_geometry) is list: + self.solid_geometry.append(LineString(points)) + return + + try: + self.solid_geometry = self.solid_geometry.union(LineString(points)) + except: + #print "Failed to run union on polygons." + log.error("Failed to run union on polylines.") + raise + def subtract_polygon(self, points): """ Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths. @@ -2756,7 +2777,7 @@ class CNCjob(Geometry): # so we actually are sorting the tools by diameter sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1]) if tools == "all": - tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools + tools = [i[0] for i in sorted_tools] # we get a array of ordered tools log.debug("Tools 'all' and sorted are: %s" % str(tools)) else: selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ',' @@ -2797,24 +2818,26 @@ class CNCjob(Geometry): for tool in tools: - # Tool change sequence (optional) - if toolchange: - gcode += "G00 Z%.4f\n" % toolchangez - gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer) - gcode += "M5\n" # Spindle Stop - gcode += "M6\n" # Tool change - gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"] - gcode += "M0\n" # Temporary machine stop - if self.spindlespeed is not None: - gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed - else: - gcode += "M03\n" # Spindle start + # only if tool have some points, otherwise thre may be error and this part is useless + if tool in points: + # Tool change sequence (optional) + if toolchange: + gcode += "G00 Z%.4f\n" % toolchangez + gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer) + gcode += "M5\n" # Spindle Stop + gcode += "M6\n" # Tool change + gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"] + gcode += "M0\n" # Temporary machine stop + if self.spindlespeed is not None: + gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed + else: + gcode += "M03\n" # Spindle start - # Drillling! - for point in points[tool]: - x, y = point.coords.xy - gcode += t % (x[0], y[0]) - gcode += down + up + # Drillling! + for point in points[tool]: + x, y = point.coords.xy + gcode += t % (x[0], y[0]) + gcode += down + up gcode += t % (0, 0) gcode += "M05\n" # Spindle stop diff --git a/camlib.pyc b/camlib.pyc index c70f7cec..7f788a0a 100644 Binary files a/camlib.pyc and b/camlib.pyc differ diff --git a/descartes/__init__.pyc b/descartes/__init__.pyc index 2433c763..8fec3939 100644 Binary files a/descartes/__init__.pyc and b/descartes/__init__.pyc differ diff --git a/descartes/patch.pyc b/descartes/patch.pyc index c8b49633..ca587af8 100644 Binary files a/descartes/patch.pyc and b/descartes/patch.pyc differ diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py new file mode 100644 index 00000000..b93ec752 --- /dev/null +++ b/tclCommands/TclCommand.py @@ -0,0 +1,394 @@ +import sys +import re +import FlatCAMApp +import abc +import collections +from PyQt4 import QtCore +from contextlib import contextmanager + + +class TclCommand(object): + + # FlatCAMApp + app = None + + # logger + log = None + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = [] + + # dictionary of types from Tcl command, needs to be ordered + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) + option_types = collections.OrderedDict() + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) + help = { + 'main': "undefined help.", + 'args': collections.OrderedDict([ + ('argumentname', 'undefined help.'), + ('optionname', 'undefined help.') + ]), + 'examples': [] + } + + # original incoming arguments into command + original_args = None + + def __init__(self, app): + self.app = app + if self.app is None: + raise TypeError('Expected app to be FlatCAMApp instance.') + if not isinstance(self.app, FlatCAMApp.App): + raise TypeError('Expected FlatCAMApp, got %s.' % type(app)) + self.log = self.app.log + + def raise_tcl_error(self, text): + """ + this method pass exception from python into TCL as error, so we get stacktrace and reason + this is only redirect to self.app.raise_tcl_error + :param text: text of error + :return: none + """ + + self.app.raise_tcl_error(text) + + def get_current_command(self): + """ + get current command, we are not able to get it from TCL we have to reconstruct it + :return: current command + """ + command_string = [] + command_string.append(self.aliases[0]) + if self.original_args is not None: + for arg in self.original_args: + command_string.append(arg) + return " ".join(command_string) + + def get_decorated_help(self): + """ + Decorate help for TCL console output. + + :return: decorated help from structure + """ + + def get_decorated_command(alias_name): + command_string = [] + for arg_key, arg_type in self.help['args'].items(): + command_string.append(get_decorated_argument(arg_key, arg_type, True)) + return "> " + alias_name + " " + " ".join(command_string) + + def get_decorated_argument(help_key, help_text, in_command=False): + option_symbol = '' + if help_key in self.arg_names: + arg_type = self.arg_names[help_key] + type_name = str(arg_type.__name__) + in_command_name = "<" + type_name + ">" + elif help_key in self.option_types: + option_symbol = '-' + arg_type = self.option_types[help_key] + type_name = str(arg_type.__name__) + in_command_name = option_symbol + help_key + " <" + type_name + ">" + else: + option_symbol = '' + type_name = '?' + in_command_name = option_symbol + help_key + " <" + type_name + ">" + + if in_command: + if help_key in self.required: + return in_command_name + else: + return '[' + in_command_name + "]" + else: + if help_key in self.required: + return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text + else: + return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]" + + def get_decorated_example(example_item): + return "> "+example_item + + help_string = [self.help['main']] + for alias in self.aliases: + help_string.append(get_decorated_command(alias)) + + for key, value in self.help['args'].items(): + help_string.append(get_decorated_argument(key, value)) + + # timeout is unique for signaled commands (this is not best oop practice, but much easier for now) + if isinstance(self, TclCommandSignaled): + help_string.append("\t[-timeout : Max wait for job timeout before error.]") + + for example in self.help['examples']: + help_string.append(get_decorated_example(example)) + + return "\n".join(help_string) + + @staticmethod + def parse_arguments(args): + """ + Pre-processes arguments to detect '-keyword value' pairs into dictionary + and standalone parameters into list. + + This is copy from FlatCAMApp.setup_shell().h() just for accessibility, + original should be removed after all commands will be converted + + :param args: arguments from tcl to parse + :return: arguments, options + """ + + options = {} + arguments = [] + n = len(args) + name = None + for i in range(n): + match = re.search(r'^-([a-zA-Z].*)', args[i]) + if match: + assert name is None + name = match.group(1) + continue + + if name is None: + arguments.append(args[i]) + else: + options[name] = args[i] + name = None + + return arguments, options + + def check_args(self, args): + """ + Check arguments and options for right types + + :param args: arguments from tcl to check + :return: named_args, unnamed_args + """ + + arguments, options = self.parse_arguments(args) + + named_args = {} + unnamed_args = [] + + # check arguments + idx = 0 + arg_names_items = self.arg_names.items() + for argument in arguments: + if len(self.arg_names) > idx: + key, arg_type = arg_names_items[idx] + try: + named_args[key] = arg_type(argument) + except Exception, e: + self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'." + % (key, arg_type, str(e))) + else: + unnamed_args.append(argument) + idx += 1 + + # check options + for key in options: + if key not in self.option_types and key != 'timeout': + self.raise_tcl_error('Unknown parameter: %s' % key) + try: + if key != 'timeout': + named_args[key] = self.option_types[key](options[key]) + else: + named_args[key] = int(options[key]) + except Exception, e: + self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'." + % (key, self.option_types[key], str(e))) + + # check required arguments + for key in self.required: + if key not in named_args: + self.raise_tcl_error("Missing required argument '%s'." % key) + + return named_args, unnamed_args + + + def raise_tcl_unknown_error(self, unknownException): + """ + raise Exception if is different type than TclErrorException + this is here mainly to show unknown errors inside TCL shell console + :param unknownException: + :return: + """ + + #if not isinstance(unknownException, self.TclErrorException): + # self.raise_tcl_error("Unknown error: %s" % str(unknownException)) + #else: + raise unknownException + + def raise_tcl_error(self, text): + """ + this method pass exception from python into TCL as error, so we get stacktrace and reason + :param text: text of error + :return: raise exception + """ + + # becouse of signaling we cannot call error to TCL from here but when task is finished + # also nonsiglaned arwe handled here to better exception handling and diplay after command is finished + raise self.app.TclErrorException(text) + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + + #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]}) + + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) + self.original_args=args + args, unnamed_args = self.check_args(args) + return self.execute(args, unnamed_args) + except Exception as unknown: + error_info=sys.exc_info() + self.log.error("TCL command '%s' failed." % str(self)) + self.app.display_tcl_error(unknown, error_info) + self.raise_tcl_unknown_error(unknown) + + @abc.abstractmethod + def execute(self, args, unnamed_args): + """ + Direct execute of command, this method should be implemented in each descendant. + No main catch should be implemented here. + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None, output text or exception + """ + + raise NotImplementedError("Please Implement this method") + +class TclCommandSignaled(TclCommand): + """ + !!! I left it here only for demonstration !!! + Go to TclCommandCncjob and into class definition put + class TclCommandCncjob(TclCommand.TclCommandSignaled): + also change + obj.generatecncjob(use_thread = False, **args) + to + obj.generatecncjob(use_thread = True, **args) + + + This class is child of TclCommand and is used for commands which create new objects + it handles all neccessary stuff about blocking and passing exeptions + """ + + output = None + + def execute_call(self, args, unnamed_args): + + try: + self.output = None + self.error=None + self.error_info=None + self.output = self.execute(args, unnamed_args) + except Exception as unknown: + self.error_info = sys.exc_info() + self.error=unknown + finally: + self.app.shell_command_finished.emit(self) + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + + @contextmanager + def wait_signal(signal, timeout=300000): + """Block loop until signal emitted, or timeout (ms) elapses.""" + loop = QtCore.QEventLoop() + + # Normal termination + signal.connect(loop.quit) + + # Termination by exception in thread + self.app.thread_exception.connect(loop.quit) + + status = {'timed_out': False} + + def report_quit(): + status['timed_out'] = True + loop.quit() + + yield + + # Temporarily change how exceptions are managed. + oeh = sys.excepthook + ex = [] + + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout + if timeout is not None: + QtCore.QTimer.singleShot(timeout, report_quit) + + # Block + loop.exec_() + + # Restore exception management + sys.excepthook = oeh + if ex: + raise ex[0] + + if status['timed_out']: + self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout ' for command or 'set_sys background_timeout '.") + + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) + self.original_args=args + args, unnamed_args = self.check_args(args) + if 'timeout' in args: + passed_timeout=args['timeout'] + del args['timeout'] + else: + passed_timeout= self.app.defaults['background_timeout'] + + # set detail for processing, it will be there until next open or close + self.app.shell.open_proccessing(self.get_current_command()) + + def handle_finished(obj): + self.app.shell_command_finished.disconnect(handle_finished) + if self.error is not None: + self.raise_tcl_unknown_error(self.error) + + self.app.shell_command_finished.connect(handle_finished) + + with wait_signal(self.app.shell_command_finished, passed_timeout): + # every TclCommandNewObject ancestor support timeout as parameter, + # but it does not mean anything for child itself + # when operation will be really long is good to set it higher then defqault 30s + self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) + + return self.output + + except Exception as unknown: + error_info=sys.exc_info() + self.log.error("TCL command '%s' failed." % str(self)) + self.app.display_tcl_error(unknown, error_info) + self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py new file mode 100644 index 00000000..c9e35078 --- /dev/null +++ b/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 [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() diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py new file mode 100644 index 00000000..3c994760 --- /dev/null +++ b/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 [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() diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py new file mode 100644 index 00000000..e6d84de3 --- /dev/null +++ b/tclCommands/TclCommandCncjob.py @@ -0,0 +1,80 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandCncjob(TclCommand.TclCommandSignaled): + """ + Tcl shell command to Generates a CNC Job from a Geometry Object. + + example: + set_sys units MM + new + open_gerber tests/gerber_files/simple1.gbr -outname margin + isolate margin -dia 3 + cncjob margin_iso + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['cncjob'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('z_cut',float), + ('z_move',float), + ('feedrate',float), + ('tooldia',float), + ('spindlespeed',int), + ('multidepth',bool), + ('depthperpass',float), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Generates a CNC Job from a Geometry Object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('z_cut', 'Z-axis cutting position.'), + ('z_move', 'Z-axis moving position.'), + ('feedrate', 'Moving speed when cutting.'), + ('tooldia', 'Tool diameter to show on screen.'), + ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), + ('multidepth', 'Use or not multidepth cnccut.'), + ('depthperpass', 'Height of one layer for multidepth.'), + ('outname', 'Name of the resulting Geometry object.') + ]), + 'examples': [] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_cnc" + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, FlatCAMGeometry): + self.raise_tcl_error('Expected FlatCAMGeometry, got %s %s.' % (name, type(obj))) + + del args['name'] + obj.generatecncjob(use_thread = False, **args) \ No newline at end of file diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py new file mode 100644 index 00000000..783b6599 --- /dev/null +++ b/tclCommands/TclCommandDrillcncjob.py @@ -0,0 +1,81 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): + """ + Tcl shell command to Generates a Drill CNC Job from a Excellon Object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['drillcncjob'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('tools',str), + ('drillz',float), + ('travelz',float), + ('feedrate',float), + ('spindlespeed',int), + ('toolchange',bool), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Generates a Drill CNC Job from a Excellon Object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'), + ('drillz', 'Drill depth into material (example: -2.0).'), + ('travelz', 'Travel distance above material (example: 2.0).'), + ('feedrate', 'Drilling feed rate.'), + ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), + ('toolchange', 'Enable tool changes (example: True).'), + ('outname', 'Name of the resulting Geometry object.') + ]), + 'examples': [] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_cnc" + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, FlatCAMExcellon): + self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj))) + + def job_init(job_obj, app): + job_obj.z_cut = args["drillz"] + job_obj.z_move = args["travelz"] + job_obj.feedrate = args["feedrate"] + job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None + toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False + tools = args["tools"] if "tools" in args else 'all' + job_obj.generate_from_excellon_by_tool(obj, tools, toolchange) + job_obj.gcode_parse() + job_obj.create_geometry() + + self.app.new_object("cncjob", args['outname'], job_init) diff --git a/tclCommands/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py new file mode 100644 index 00000000..feecd870 --- /dev/null +++ b/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) diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py new file mode 100644 index 00000000..ac69e7cb --- /dev/null +++ b/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) diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py new file mode 100644 index 00000000..61bfe9f0 --- /dev/null +++ b/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) diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py new file mode 100644 index 00000000..8c51f21e --- /dev/null +++ b/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) diff --git a/tclCommands/TclCommandNew.py b/tclCommands/TclCommandNew.py new file mode 100644 index 00000000..db3fe576 --- /dev/null +++ b/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() diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py new file mode 100644 index 00000000..a951d8f3 --- /dev/null +++ b/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) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py new file mode 100644 index 00000000..2f733017 --- /dev/null +++ b/tclCommands/__init__.py @@ -0,0 +1,52 @@ +import pkgutil +import sys + +# allowed command modules (please append them alphabetically ordered) +import tclCommands.TclCommandAddPolygon +import tclCommands.TclCommandAddPolyline +import tclCommands.TclCommandCncjob +import tclCommands.TclCommandDrillcncjob +import tclCommands.TclCommandExportGcode +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors +import tclCommands.TclCommandIsolate +import tclCommands.TclCommandNew +import tclCommands.TclCommandOpenGerber + + +__all__ = [] + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + module = loader.find_module(name).load_module(name) + __all__.append(name) + +def register_all_commands(app, commands): + """ + Static method which register all known commands. + + Command should be for now in directory tclCommands and module should start with TCLCommand + Class have to follow same name as module. + + we need import all modules in top section: + import tclCommands.TclCommandExteriors + at this stage we can include only wanted commands with this, auto loading may be implemented in future + I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. + + :param app: FlatCAMApp + :param commands: array of commands which should be modified + :return: None + """ + + tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} + + for key, mod in tcl_modules.items(): + if key != 'tclCommands.TclCommand': + class_name = key.split('.')[1] + class_type = getattr(mod, class_name) + command_instance = class_type(app) + + for alias in command_instance.aliases: + commands[alias] = { + 'fcn': command_instance.execute_wrapper, + 'help': command_instance.get_decorated_help() + } diff --git a/termwidget.py b/termwidget.py index b2e4fdba..538cc161 100644 --- a/termwidget.py +++ b/termwidget.py @@ -4,8 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history. """ import cgi - -from PyQt4.QtCore import pyqtSignal +from PyQt4.QtCore import pyqtSignal, Qt from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ QSizePolicy, QTextCursor, QTextEdit, \ QVBoxLayout, QWidget @@ -83,7 +82,6 @@ class _ExpandableTextEdit(QTextEdit): # Paste only plain text. self.insertPlainText(mime_data.text()) - class TermWidget(QWidget): """ Widget wich represents terminal. It only displays text and allows to enter text. @@ -118,6 +116,34 @@ class TermWidget(QWidget): self._edit.setFocus() + def open_proccessing(self, detail=None): + """ + Open processing and disable using shell commands again until all commands are finished + + :param detail: text detail about what is currently called from TCL to python + :return: None + """ + + self._edit.setTextColor(Qt.white) + self._edit.setTextBackgroundColor(Qt.darkGreen) + if detail is None: + self._edit.setPlainText("...proccessing...") + else: + self._edit.setPlainText("...proccessing... [%s]" % detail) + + self._edit.setDisabled(True) + + def close_proccessing(self): + """ + Close processing and enable using shell commands again + :return: + """ + + self._edit.setTextColor(Qt.black) + self._edit.setTextBackgroundColor(Qt.white) + self._edit.setPlainText('') + self._edit.setDisabled(False) + def _append_to_browser(self, style, text): """ Convert text to HTML for inserting it to browser @@ -225,4 +251,3 @@ class TermWidget(QWidget): self._historyIndex -= 1 self._edit.setPlainText(self._history[self._historyIndex]) self._edit.moveCursor(QTextCursor.End) - diff --git a/tests/gerber_files/detector_contour.gbr b/tests/gerber_files/detector_contour.gbr new file mode 100644 index 00000000..93adef01 --- /dev/null +++ b/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* \ No newline at end of file diff --git a/tests/gerber_files/detector_copper_bottom.gbr b/tests/gerber_files/detector_copper_bottom.gbr new file mode 100644 index 00000000..d3bca481 --- /dev/null +++ b/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* \ No newline at end of file diff --git a/tests/gerber_files/detector_copper_top.gbr b/tests/gerber_files/detector_copper_top.gbr new file mode 100644 index 00000000..52b2e2ae --- /dev/null +++ b/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* \ No newline at end of file diff --git a/tests/gerber_files/detector_drill.txt b/tests/gerber_files/detector_drill.txt new file mode 100644 index 00000000..c4945b84 --- /dev/null +++ b/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 diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py new file mode 100644 index 00000000..d36f30ed --- /dev/null +++ b/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 \ No newline at end of file