From 6b527fa256f9059383caa5ce48ff6e4bf247a1ea Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 10 Mar 2016 16:01:50 +0100 Subject: [PATCH 01/22] example howto handle Exceptions in shell --- FlatCAMApp.py | 86 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d1fcf450..63e8fb78 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -644,14 +644,32 @@ class App(QtCore.QObject): else: self.defaults['stats'][resource] = 1 + class TclErrorException(Exception): + """ + this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command + """ + + pass + + def raiseTclUnknownError(self, unknownException): + """ + raise Exception if is different type than TclErrorException + :param unknownException: + :return: + """ + + if not isinstance(unknownException, self.TclErrorException): + self.raiseTclError("Unknown error: %s" % str(unknownException)) + def raiseTclError(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) + raise self.TclErrorException(text) def exec_command(self, text): """ @@ -660,6 +678,7 @@ class App(QtCore.QObject): :param text: Input command :return: None """ + self.report_usage('exec_command') text = str(text) @@ -2659,44 +2678,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.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + + if name is None: + self.raiseTclError('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.raiseTclError("Could not retrieve object: %s" % name) - if name is None: - self.raiseTclError('Argument name is missing.') + if obj is None: + self.raiseTclError("Object not found: %s" % name) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + if not isinstance(obj, Geometry): + self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) - if obj is None: - self.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.raiseTclError("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)) + except Exception as unknown: + self.raiseTclUnknownError(unknown) return 'Ok' From fd1c8afef9bb0a9953962ad33a8663acee1e9408 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 11 Mar 2016 01:50:12 +0100 Subject: [PATCH 02/22] implement basic set of tests for tcl_shell, need to be completed --- FlatCAMApp.py | 412 ++-- tests/gerber_files/detector_contour.gbr | 26 + tests/gerber_files/detector_copper_bottom.gbr | 2146 +++++++++++++++++ tests/gerber_files/detector_copper_top.gbr | 71 + tests/gerber_files/detector_drill.txt | 46 + tests/test_tcl_shell.py | 155 ++ 6 files changed, 2666 insertions(+), 190 deletions(-) create mode 100644 tests/gerber_files/detector_contour.gbr create mode 100644 tests/gerber_files/detector_copper_bottom.gbr create mode 100644 tests/gerber_files/detector_copper_top.gbr create mode 100644 tests/gerber_files/detector_drill.txt create mode 100644 tests/test_tcl_shell.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 63e8fb78..7729cb61 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -675,8 +675,19 @@ class App(QtCore.QObject): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + :param text: + :return: output if there was any + """ + + return self.exec_command_test(self, text, False) + + 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') @@ -691,8 +702,10 @@ class App(QtCore.QObject): 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 + return result """ Code below is unsused. Saved for later. @@ -2167,85 +2180,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.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + + try: + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("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.raiseTclUnknownError(unknown) def mirror(name, *args): a, kwa = h(*args) @@ -2519,59 +2543,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.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) - 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.raiseTclError("Could not retrieve object: %s" % name) + + 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("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + self.raiseTclError("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.raiseTclUnknownError(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): ''' @@ -2580,48 +2608,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.raiseTclError('Argument name is missing.') + + 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])) - 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.raiseTclError("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.raiseTclError("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.raiseTclError("Object not found: %s" % name) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + if not isinstance(obj, FlatCAMExcellon): + self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) - if not isinstance(obj, FlatCAMExcellon): - self.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.raiseTclError("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.raiseTclError(msg) - if not success: - self.raiseTclError(msg) - - return 'Ok' + except Exception as unknown: + self.raiseTclUnknownError(unknown) def exteriors(name=None, *args): ''' @@ -2630,46 +2661,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.raiseTclError('Argument name is missing.') + + 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])) - 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.raiseTclError("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.raiseTclError("Object not found: %s" % name) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + if not isinstance(obj, Geometry): + self.raiseTclError('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.raiseTclError("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.raiseTclUnknownError(unknown) def interiors(name=None, *args): ''' @@ -2722,8 +2756,6 @@ class App(QtCore.QObject): except Exception as unknown: self.raiseTclUnknownError(unknown) - return 'Ok' - def isolate(name=None, *args): ''' TCL shell command - see help section 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..51138b1c --- /dev/null +++ b/tests/test_tcl_shell.py @@ -0,0 +1,155 @@ +import sys +import unittest +from PyQt4 import QtGui +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 TclShellCommandTest(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 + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + def tearDown(self): + del self.fc + del self.app + + def test_set_get_units(self): + + 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_excellon_flow(self): + + + self.fc.exec_command_test('set_sys units MM') + 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 From 4df46df19b13ba364dacbefa6d0c1a1c96bcc1ae Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 11 Mar 2016 20:32:48 +0100 Subject: [PATCH 03/22] remove line fix crazzy selfness ;)... --- FlatCAMApp.py | 2 +- tests/test_tcl_shell.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 7729cb61..9f2fb815 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -679,7 +679,7 @@ class App(QtCore.QObject): :return: output if there was any """ - return self.exec_command_test(self, text, False) + return self.exec_command_test(text, False) def exec_command_test(self, text, reraise=True): """ diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 51138b1c..3d75f6aa 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -141,7 +141,6 @@ class TclShellCommandTest(unittest.TestCase): def test_excellon_flow(self): - self.fc.exec_command_test('set_sys units MM') 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) From cd6700152c0b83f3b909583695d6aa2f0b55c911 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 16 Mar 2016 18:57:43 +0100 Subject: [PATCH 04/22] draft for reimplementation of tcl commands to separated files/modules --- FlatCAMApp.py | 7 +- tclCommands/TclCommand.py | 180 +++++++++++++++++++++++++++++ tclCommands/TclCommandExteriors.py | 64 ++++++++++ tclCommands/TclCommandInteriors.py | 63 ++++++++++ tclCommands/__init__.py | 46 ++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tclCommands/TclCommand.py create mode 100644 tclCommands/TclCommandExteriors.py create mode 100644 tclCommands/TclCommandInteriors.py create mode 100644 tclCommands/__init__.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9f2fb815..5764f33d 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -25,7 +25,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +import tclCommands ######################################## ## App ## @@ -660,6 +660,8 @@ class App(QtCore.QObject): if not isinstance(unknownException, self.TclErrorException): self.raiseTclError("Unknown error: %s" % str(unknownException)) + else: + raise unknownException def raiseTclError(self, text): """ @@ -3547,6 +3549,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/tclCommands/TclCommand.py b/tclCommands/TclCommand.py new file mode 100644 index 00000000..ccc29eb2 --- /dev/null +++ b/tclCommands/TclCommand.py @@ -0,0 +1,180 @@ +import sys, inspect, pkgutil +import re +import FlatCAMApp + + +class TclCommand(object): + + app=None + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = None + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "undefined help.", + 'args': { + 'argumentname': 'undefined help.', + 'optionname': 'undefined help.' + }, + 'examples' : [] + } + + def __init__(self, app): + self.app=app + + def get_decorated_help(self): + """ + Decorate help for TCL console output. + + :return: decorated help from structue + """ + + def get_decorated_command(alias): + command_string = [] + for key, value in reversed(self.help['args'].items()): + command_string.append(get_decorated_argument(key, value, True)) + return "> " + alias + " " + " ".join(command_string) + + def get_decorated_argument(key, value, in_command=False): + option_symbol = '' + if key in self.arg_names: + type=self.arg_names[key] + in_command_name = "<" + str(type.__name__) + ">" + else: + option_symbol = '-' + type=self.option_types[key] + in_command_name = option_symbol + key + " <" + str(type.__name__) + ">" + if in_command: + if key in self.required: + return in_command_name + else: + return '[' + in_command_name + "]" + else: + if key in self.required: + return "\t" + option_symbol + key + " <" + str(type.__name__) + ">: " + value + else: + return "\t[" + option_symbol + key + " <" + str(type.__name__) + ">: " + value+"]" + + def get_decorated_example(example): + example_string = '' + return "todo" + example_string + + help_string=[self.help['main']] + for alias in self.aliases: + help_string.append(get_decorated_command(alias)) + + for key, value in reversed(self.help['args'].items()): + help_string.append(get_decorated_argument(key, value)) + + for example in self.help['examples']: + help_string.append(get_decorated_example(example)) + + return "\n".join(help_string) + + def parse_arguments(self, 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 accesibility, original should be removed after all commands will be converted + """ + + 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: + """ + + arguments, options = self.parse_arguments(args) + + named_args={} + unnamed_args=[] + + # check arguments + idx=0 + arg_names_reversed=self.arg_names.items() + for argument in arguments: + if len(self.arg_names) > idx: + key, type = arg_names_reversed[len(self.arg_names)-idx-1] + try: + named_args[key] = type(argument) + except Exception, e: + self.app.raiseTclError("Cannot cast named argument '%s' to type %s." % (key, type)) + else: + unnamed_args.append(argument) + idx += 1 + + # check otions + for key in options: + if key not in self.option_types: + self.app.raiseTclError('Unknown parameter: %s' % key) + try: + named_args[key] = self.option_types[key](options[key]) + except Exception, e: + self.app.raiseTclError("Cannot cast argument '-%s' to type %s." % (key, self.option_types[key])) + + # check required arguments + for key in self.required: + if key not in named_args: + self.app.raiseTclError("Missing required argument '%s'." % (key)) + + return named_args, unnamed_args + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + try: + args, unnamed_args = self.check_args(args) + return self.execute(args, unnamed_args) + except Exception as unknown: + self.app.raiseTclUnknownError(unknown) + + 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") diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py new file mode 100644 index 00000000..aad0fa2a --- /dev/null +++ b/tclCommands/TclCommandExteriors.py @@ -0,0 +1,64 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandExteriors(TclCommand.TclCommand): + """ + 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: args = {'name': str}, this is for value without optionname + arg_names = {'name': str,'name2': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get exteriors of polygons.", + 'args': { + '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" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('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) \ No newline at end of file diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py new file mode 100644 index 00000000..f1c65590 --- /dev/null +++ b/tclCommands/TclCommandInteriors.py @@ -0,0 +1,63 @@ +from ObjectCollection import * +import TclCommand + +class TclCommandInteriors(TclCommand.TclCommand): + """ + 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: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get interiors of polygons.", + 'args': { + '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" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('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) \ No newline at end of file diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py new file mode 100644 index 00000000..43cb0a6b --- /dev/null +++ b/tclCommands/__init__.py @@ -0,0 +1,46 @@ +import pkgutil +import inspect +import sys + +# allowed command modules +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors + + +__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, autoloading may be implemented in future + I have no enought 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, module in tcl_modules.items(): + if key != 'tclCommands.TclCommand': + classname = key.split('.')[1] + class_ = getattr(module, classname) + commandInstance=class_(app) + + for alias in commandInstance.aliases: + commands[alias]={ + 'fcn': commandInstance.execute_wrapper, + 'help': commandInstance.get_decorated_help() + } \ No newline at end of file From 2e51c1e9cd00aa404d426fe291da96c8423b2296 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 17 Mar 2016 10:54:01 +0100 Subject: [PATCH 05/22] hide showing 'None' if command end sucessfully --- FlatCAMApp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5764f33d..263580fe 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -698,7 +698,8 @@ class App(QtCore.QObject): try: 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") From 78854f7fe04e6eb98627fe710fa082253ade7121 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 17 Mar 2016 12:14:12 +0100 Subject: [PATCH 06/22] fix ordering in naming arguments and help add commands TclCommandAddPolygon(add_poly, add_polygon) and TclCommandAddPolyline(add_polyline) implement add_polyline in camlib.py --- camlib.py | 21 +++++++++ tclCommands/TclCommand.py | 52 ++++++++++++---------- tclCommands/TclCommandAddPolygon.py | 65 ++++++++++++++++++++++++++++ tclCommands/TclCommandAddPolyline.py | 65 ++++++++++++++++++++++++++++ tclCommands/TclCommandExteriors.py | 22 ++++++---- tclCommands/TclCommandInteriors.py | 22 ++++++---- tclCommands/__init__.py | 2 + 7 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 tclCommands/TclCommandAddPolygon.py create mode 100644 tclCommands/TclCommandAddPolyline.py diff --git a/camlib.py b/camlib.py index f576ed98..5a8487a3 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. diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index ccc29eb2..eb8e222a 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,31 +1,33 @@ import sys, inspect, pkgutil import re import FlatCAMApp - +import collections class TclCommand(object): app=None # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = None + aliases = [] - # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname - arg_names = {'name': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {} + # 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 + # structured help for current command, args needs to be ordered help = { 'main': "undefined help.", - 'args': { - 'argumentname': 'undefined help.', - 'optionname': 'undefined help.' - }, + 'args': collections.OrderedDict([ + ('argumentname', 'undefined help.'), + ('optionname', 'undefined help.') + ]), 'examples' : [] } @@ -41,7 +43,7 @@ class TclCommand(object): def get_decorated_command(alias): command_string = [] - for key, value in reversed(self.help['args'].items()): + for key, value in self.help['args'].items(): command_string.append(get_decorated_argument(key, value, True)) return "> " + alias + " " + " ".join(command_string) @@ -49,11 +51,18 @@ class TclCommand(object): option_symbol = '' if key in self.arg_names: type=self.arg_names[key] - in_command_name = "<" + str(type.__name__) + ">" - else: + type_name=str(type.__name__) + in_command_name = "<" + type_name + ">" + elif key in self.option_types: option_symbol = '-' type=self.option_types[key] - in_command_name = option_symbol + key + " <" + str(type.__name__) + ">" + type_name=str(type.__name__) + in_command_name = option_symbol + key + " <" + type_name + ">" + else: + option_symbol = '' + type_name='?' + in_command_name = option_symbol + key + " <" + type_name + ">" + if in_command: if key in self.required: return in_command_name @@ -61,19 +70,18 @@ class TclCommand(object): return '[' + in_command_name + "]" else: if key in self.required: - return "\t" + option_symbol + key + " <" + str(type.__name__) + ">: " + value + return "\t" + option_symbol + key + " <" + type_name + ">: " + value else: - return "\t[" + option_symbol + key + " <" + str(type.__name__) + ">: " + value+"]" + return "\t[" + option_symbol + key + " <" + type_name + ">: " + value+"]" def get_decorated_example(example): - example_string = '' - return "todo" + example_string + return "> "+example help_string=[self.help['main']] for alias in self.aliases: help_string.append(get_decorated_command(alias)) - for key, value in reversed(self.help['args'].items()): + for key, value in self.help['args'].items(): help_string.append(get_decorated_argument(key, value)) for example in self.help['examples']: @@ -123,10 +131,10 @@ class TclCommand(object): # check arguments idx=0 - arg_names_reversed=self.arg_names.items() + arg_names_items=self.arg_names.items() for argument in arguments: if len(self.arg_names) > idx: - key, type = arg_names_reversed[len(self.arg_names)-idx-1] + key, type = arg_names_items[idx] try: named_args[key] = type(argument) except Exception, e: diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py new file mode 100644 index 00000000..b5effcd8 --- /dev/null +++ b/tclCommands/TclCommandAddPolygon.py @@ -0,0 +1,65 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandAddPolygon(TclCommand.TclCommand): + """ + 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'] + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + if len(unnamed_args) % 2 != 0: + self.app.raiseTclError("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() \ No newline at end of file diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py new file mode 100644 index 00000000..157f6e1f --- /dev/null +++ b/tclCommands/TclCommandAddPolyline.py @@ -0,0 +1,65 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandAddPolyline(TclCommand.TclCommand): + """ + 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'] + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + if len(unnamed_args) % 2 != 0: + self.app.raiseTclError("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() \ No newline at end of file diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py index aad0fa2a..d445cd5c 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -10,22 +10,26 @@ class TclCommandExteriors(TclCommand.TclCommand): # 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: args = {'name': str}, this is for value without optionname - arg_names = {'name': str,'name2': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {'outname': 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 + # structured help for current command, args needs to be ordered help = { 'main': "Get exteriors of polygons.", - 'args': { - 'name': 'Name of the source Geometry object.', - 'outname': 'Name of the resulting Geometry object.' - }, + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('outname', 'Name of the resulting Geometry object.') + ]), 'examples':[] } diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index f1c65590..ef67ce9c 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -9,22 +9,26 @@ class TclCommandInteriors(TclCommand.TclCommand): # 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: args = {'name': str}, this is for value without optionname - arg_names = {'name': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {'outname': 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 + # structured help for current command, args needs to be ordered help = { 'main': "Get interiors of polygons.", - 'args': { - 'name': 'Name of the source Geometry object.', - 'outname': 'Name of the resulting Geometry object.' - }, + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('outname', 'Name of the resulting Geometry object.') + ]), 'examples':[] } diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 43cb0a6b..45d0ffcf 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -5,6 +5,8 @@ import sys # allowed command modules import tclCommands.TclCommandExteriors import tclCommands.TclCommandInteriors +import tclCommands.TclCommandAddPolygon +import tclCommands.TclCommandAddPolyline __all__=[] From 980638630d53ead63f9c1952be74eb9ccaa2743c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 19 Mar 2016 15:13:07 +0100 Subject: [PATCH 07/22] cleanups implement TclCommand.TclCommandSignaled as proof of concept (not usefull) bypass using threads within obj.generatecncjob(use_thread = False, **args) reimplement some more shell commands to OOP style --- FlatCAMApp.py | 102 +++++++------- FlatCAMObj.py | 31 +++-- tclCommands/TclCommand.py | 190 +++++++++++++++++++++------ tclCommands/TclCommandAddPolygon.py | 20 ++- tclCommands/TclCommandAddPolyline.py | 18 +-- tclCommands/TclCommandCncjob.py | 86 ++++++++++++ tclCommands/TclCommandExportGcode.py | 79 +++++++++++ tclCommands/TclCommandExteriors.py | 18 +-- tclCommands/TclCommandInteriors.py | 17 +-- tclCommands/__init__.py | 28 ++-- 10 files changed, 427 insertions(+), 162 deletions(-) create mode 100644 tclCommands/TclCommandCncjob.py create mode 100644 tclCommands/TclCommandExportGcode.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0090ad02..840cf425 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -651,7 +651,7 @@ class App(QtCore.QObject): pass - def raiseTclUnknownError(self, unknownException): + def raise_tcl_unknown_error(self, unknownException): """ raise Exception if is different type than TclErrorException :param unknownException: @@ -659,11 +659,11 @@ class App(QtCore.QObject): """ if not isinstance(unknownException, self.TclErrorException): - self.raiseTclError("Unknown error: %s" % str(unknownException)) + self.raise_tcl_error("Unknown error: %s" % str(unknownException)) else: raise unknownException - def raiseTclError(self, 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 @@ -2274,20 +2274,20 @@ class App(QtCore.QObject): # 8 - 2*left + 2*right +2*top + 2*bottom if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') 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, str(types[key]))) + 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.raiseTclError("Could not retrieve object: %s" % name) + 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() @@ -2337,7 +2337,7 @@ class App(QtCore.QObject): ymax + gapsize) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def mirror(name, *args): a, kwa = h(*args) @@ -2624,26 +2624,26 @@ class App(QtCore.QObject): } if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') 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, str(types[key]))) + 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.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) if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) try: # Get the tools from the list @@ -2663,10 +2663,10 @@ class App(QtCore.QObject): obj.app.new_object("cncjob", job_name, job_init) except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def millholes(name=None, *args): @@ -2684,43 +2684,43 @@ class App(QtCore.QObject): 'outname': str} if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') 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: 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)) + self.raise_tcl_error("Bad tools: %s" % str(e)) 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) if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) try: success, msg = obj.generate_milling(**kwa) except Exception as e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) if not success: - self.raiseTclError(msg) + self.raise_tcl_error(msg) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def exteriors(name=None, *args): ''' @@ -2735,26 +2735,26 @@ class App(QtCore.QObject): types = {'outname': str} if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') 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) if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors @@ -2768,10 +2768,10 @@ class App(QtCore.QObject): obj_exteriors = obj.get_exteriors() self.new_object('geometry', outname, geo_init) except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) + self.raise_tcl_error("Failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def interiors(name=None, *args): ''' @@ -2787,25 +2787,25 @@ 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])) if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + 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) if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_interiors @@ -2819,10 +2819,10 @@ class App(QtCore.QObject): obj_interiors = obj.get_interiors() self.new_object('geometry', outname, geo_init) except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) + self.raise_tcl_error("Failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def isolate(name=None, *args): ''' @@ -2840,29 +2840,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' @@ -3253,11 +3253,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. 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/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index eb8e222a..7f8a7e8d 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,83 +1,108 @@ -import sys, inspect, pkgutil import re import FlatCAMApp +import abc import collections +from PyQt4 import QtCore +from contextlib import contextmanager + class TclCommand(object): - app=None + # 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 - option_types = collections.OrderedDict([]) + # 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' : [] + 'examples': [] } def __init__(self, app): - self.app=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_decorated_help(self): """ Decorate help for TCL console output. - :return: decorated help from structue + :return: decorated help from structure """ - def get_decorated_command(alias): + def get_decorated_command(alias_name): command_string = [] - for key, value in self.help['args'].items(): - command_string.append(get_decorated_argument(key, value, True)) - return "> " + alias + " " + " ".join(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(key, value, in_command=False): + def get_decorated_argument(help_key, help_text, in_command=False): option_symbol = '' - if key in self.arg_names: - type=self.arg_names[key] - type_name=str(type.__name__) + 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 key in self.option_types: + elif help_key in self.option_types: option_symbol = '-' - type=self.option_types[key] - type_name=str(type.__name__) - in_command_name = option_symbol + key + " <" + type_name + ">" + 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 + key + " <" + type_name + ">" + type_name = '?' + in_command_name = option_symbol + help_key + " <" + type_name + ">" if in_command: - if key in self.required: + if help_key in self.required: return in_command_name else: return '[' + in_command_name + "]" else: - if key in self.required: - return "\t" + option_symbol + key + " <" + type_name + ">: " + value + if help_key in self.required: + return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text else: - return "\t[" + option_symbol + key + " <" + type_name + ">: " + value+"]" + return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]" - def get_decorated_example(example): - return "> "+example + def get_decorated_example(example_item): + return "> "+example_item - help_string=[self.help['main']] + help_string = [self.help['main']] for alias in self.aliases: help_string.append(get_decorated_command(alias)) @@ -89,12 +114,17 @@ class TclCommand(object): return "\n".join(help_string) - def parse_arguments(self, args): + @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 accesibility, original should be removed after all commands will be converted + 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 = {} @@ -121,41 +151,43 @@ class TclCommand(object): Check arguments and options for right types :param args: arguments from tcl to check - :return: + :return: named_args, unnamed_args """ arguments, options = self.parse_arguments(args) - named_args={} - unnamed_args=[] + named_args = {} + unnamed_args = [] # check arguments - idx=0 - arg_names_items=self.arg_names.items() + idx = 0 + arg_names_items = self.arg_names.items() for argument in arguments: if len(self.arg_names) > idx: - key, type = arg_names_items[idx] + key, arg_type = arg_names_items[idx] try: - named_args[key] = type(argument) + named_args[key] = arg_type(argument) except Exception, e: - self.app.raiseTclError("Cannot cast named argument '%s' to type %s." % (key, type)) + 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 otions + # check options for key in options: if key not in self.option_types: - self.app.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: named_args[key] = self.option_types[key](options[key]) except Exception, e: - self.app.raiseTclError("Cannot cast argument '-%s' to type %s." % (key, self.option_types[key])) + 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.app.raiseTclError("Missing required argument '%s'." % (key)) + self.raise_tcl_error("Missing required argument '%s'." % key) return named_args, unnamed_args @@ -168,12 +200,16 @@ class TclCommand(object): :param args: arguments passed from tcl command console :return: None, output text or exception """ + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) args, unnamed_args = self.check_args(args) return self.execute(args, unnamed_args) except Exception as unknown: - self.app.raiseTclUnknownError(unknown) + self.log.error("TCL command '%s' failed." % str(self)) + self.app.raise_tcl_unknown_error(unknown) + @abc.abstractmethod def execute(self, args, unnamed_args): """ Direct execute of command, this method should be implemented in each descendant. @@ -186,3 +222,73 @@ class TclCommand(object): """ raise NotImplementedError("Please Implement this method") + + +class TclCommandSignaled(TclCommand): + """ + !!! I left it here only for demonstration !!! + Go to TclCommandCncjob and into class definition put + class TclCommandCncjob(TclCommand.TclCommandSignaled): + also change + obj.generatecncjob(use_thread = False, **args) + to + obj.generatecncjob(use_thread = True, **args) + + + This class is child of TclCommand and is used for commands which create new objects + it handles all neccessary stuff about blocking and passing exeptions + """ + + # default timeout for operation is 30 sec, but it can be much more + default_timeout = 30000 + + + 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=30000): + """Block loop until signal emitted, or timeout (ms) elapses.""" + loop = QtCore.QEventLoop() + signal.connect(loop.quit) + + status = {'timed_out': False} + + def report_quit(): + status['timed_out'] = True + loop.quit() + + yield + + if timeout is not None: + QtCore.QTimer.singleShot(timeout, report_quit) + + loop.exec_() + + if status['timed_out']: + self.app.raise_tcl_unknown_error('Operation timed out!') + + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) + args, unnamed_args = self.check_args(args) + if 'timeout' in args: + passed_timeout=args['timeout'] + del args['timeout'] + else: + passed_timeout=self.default_timeout + with wait_signal(self.app.new_object_available, 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 + return self.execute(args, unnamed_args) + + except Exception as unknown: + self.log.error("TCL command '%s' failed." % str(self)) + self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py index b5effcd8..6d2c2afd 100644 --- a/tclCommands/TclCommandAddPolygon.py +++ b/tclCommands/TclCommandAddPolygon.py @@ -8,7 +8,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): """ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['add_polygon','add_poly'] + aliases = ['add_polygon', 'add_poly'] # dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ @@ -16,7 +16,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([]) + option_types = collections.OrderedDict() # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] @@ -28,7 +28,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): ('name', 'Name of the Geometry object to which to append the polygon.'), ('xi, yi', 'Coordinates of points in the polygon.') ]), - 'examples':[ + 'examples': [ 'add_polygon [x3 y3 [...]]' ] } @@ -45,21 +45,17 @@ class TclCommandAddPolygon(TclCommand.TclCommand): name = args['name'] - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) if len(unnamed_args) % 2 != 0: - self.app.raiseTclError("Incomplete coordinates.") + 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() \ No newline at end of file + obj.plot() diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py index 157f6e1f..57b8fe02 100644 --- a/tclCommands/TclCommandAddPolyline.py +++ b/tclCommands/TclCommandAddPolyline.py @@ -16,7 +16,7 @@ class TclCommandAddPolyline(TclCommand.TclCommand): ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([]) + option_types = collections.OrderedDict() # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] @@ -28,7 +28,7 @@ class TclCommandAddPolyline(TclCommand.TclCommand): ('name', 'Name of the Geometry object to which to append the polyline.'), ('xi, yi', 'Coordinates of points in the polyline.') ]), - 'examples':[ + 'examples': [ 'add_polyline [x3 y3 [...]]' ] } @@ -45,21 +45,17 @@ class TclCommandAddPolyline(TclCommand.TclCommand): name = args['name'] - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) if len(unnamed_args) % 2 != 0: - self.app.raiseTclError("Incomplete coordinates.") + 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() \ No newline at end of file + obj.plot() diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py new file mode 100644 index 00000000..17a677ee --- /dev/null +++ b/tclCommands/TclCommandCncjob.py @@ -0,0 +1,86 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandCncjob(TclCommand.TclCommand): + """ + Tcl shell command to Generates a CNC Job from a Geometry Object. + + example: + set_sys units MM + new + open_gerber tests/gerber_files/simple1.gbr -outname margin + isolate margin -dia 3 + cncjob margin_iso + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['cncjob'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('z_cut',float), + ('z_move',float), + ('feedrate',float), + ('tooldia',float), + ('spindlespeed',int), + ('multidepth',bool), + ('depthperpass',float), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Generates a CNC Job from a Geometry Object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('z_cut', 'Z-axis cutting position.'), + ('z_move', 'Z-axis moving position.'), + ('feedrate', 'Moving speed when cutting.'), + ('tooldia', 'Tool diameter to show on screen.'), + ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), + ('multidepth', 'Use or not multidepth cnccut.'), + ('depthperpass', 'Height of one layer for multidepth.'), + ('outname', 'Name of the resulting Geometry object.'), + ('timeout', 'Max wait for job timeout before error.') + ]), + 'examples': [] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_cnc" + + 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, 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/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py new file mode 100644 index 00000000..520f6ecb --- /dev/null +++ b/tclCommands/TclCommandExportGcode.py @@ -0,0 +1,79 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandExportGcode(TclCommand.TclCommand): + """ + 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 index d445cd5c..16f2fee4 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -8,7 +8,7 @@ class TclCommandExteriors(TclCommand.TclCommand): """ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['exteriors','ext'] + aliases = ['exteriors', 'ext'] # dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ @@ -30,7 +30,7 @@ class TclCommandExteriors(TclCommand.TclCommand): ('name', 'Name of the source Geometry object.'), ('outname', 'Name of the resulting Geometry object.') ]), - 'examples':[] + 'examples': [] } def execute(self, args, unnamed_args): @@ -50,19 +50,15 @@ class TclCommandExteriors(TclCommand.TclCommand): else: outname = name + "_exteriors" - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj, app_obj): + def geo_init(geo_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_exteriors() - self.app.new_object('geometry', outname, geo_init) \ No newline at end of file + self.app.new_object('geometry', outname, geo_init) diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index ef67ce9c..2314be3f 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -1,6 +1,7 @@ from ObjectCollection import * import TclCommand + class TclCommandInteriors(TclCommand.TclCommand): """ Tcl shell command to get interiors of polygons @@ -29,7 +30,7 @@ class TclCommandInteriors(TclCommand.TclCommand): ('name', 'Name of the source Geometry object.'), ('outname', 'Name of the resulting Geometry object.') ]), - 'examples':[] + 'examples': [] } def execute(self, args, unnamed_args): @@ -49,19 +50,15 @@ class TclCommandInteriors(TclCommand.TclCommand): else: outname = name + "_interiors" - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj, app_obj): + def geo_init(geo_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_interiors() - self.app.new_object('geometry', outname, geo_init) \ No newline at end of file + self.app.new_object('geometry', outname, geo_init) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 45d0ffcf..3055dbc7 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -1,5 +1,4 @@ import pkgutil -import inspect import sys # allowed command modules @@ -7,9 +6,10 @@ import tclCommands.TclCommandExteriors import tclCommands.TclCommandInteriors import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline +import tclCommands.TclCommandExportGcode +import tclCommands.TclCommandCncjob - -__all__=[] +__all__ = [] for loader, name, is_pkg in pkgutil.walk_packages(__path__): module = loader.find_module(name).load_module(name) @@ -25,8 +25,8 @@ def register_all_commands(app, commands): we need import all modules in top section: import tclCommands.TclCommandExteriors - at this stage we can include only wanted commands with this, autoloading may be implemented in future - I have no enought knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. + 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 @@ -35,14 +35,14 @@ def register_all_commands(app, commands): tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} - for key, module in tcl_modules.items(): + for key, mod in tcl_modules.items(): if key != 'tclCommands.TclCommand': - classname = key.split('.')[1] - class_ = getattr(module, classname) - commandInstance=class_(app) + class_name = key.split('.')[1] + class_type = getattr(mod, class_name) + command_instance = class_type(app) - for alias in commandInstance.aliases: - commands[alias]={ - 'fcn': commandInstance.execute_wrapper, - 'help': commandInstance.get_decorated_help() - } \ No newline at end of file + for alias in command_instance.aliases: + commands[alias] = { + 'fcn': command_instance.execute_wrapper, + 'help': command_instance.get_decorated_help() + } From e96ee1af29a791972897135003d119172a1d0ebf Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 24 Mar 2016 23:06:44 +0100 Subject: [PATCH 08/22] merge new pull requests from FlatCAM->master implement executing of tasks inside worker thread cleanups, reimplement Isolate/New/OpenGerber as OOP style Shell commands disable edit during shell execution, show some progress add ability for breakpoints in other threads and only if available add X11 safe flag, not sure what happen on windows --- FlatCAM.py | 5 + FlatCAMApp.py | 244 ++++++++++++++++++++++++--- FlatCAMGUI.py | 4 + FlatCAMShell.py | 2 +- FlatCAMWorker.py | 28 ++- camlib.py | 68 ++++++++ tclCommands/TclCommand.py | 53 +++++- tclCommands/TclCommandAddPolygon.py | 2 +- tclCommands/TclCommandAddPolyline.py | 2 +- tclCommands/TclCommandCncjob.py | 7 +- tclCommands/TclCommandExportGcode.py | 2 +- tclCommands/TclCommandExteriors.py | 4 +- tclCommands/TclCommandInteriors.py | 4 +- tclCommands/TclCommandIsolate.py | 79 +++++++++ tclCommands/TclCommandNew.py | 40 +++++ tclCommands/TclCommandOpenGerber.py | 95 +++++++++++ tclCommands/__init__.py | 10 +- termwidget.py | 28 ++- tests/test_tcl_shell.py | 24 ++- 19 files changed, 651 insertions(+), 50 deletions(-) create mode 100644 tclCommands/TclCommandIsolate.py create mode 100644 tclCommands/TclCommandNew.py create mode 100644 tclCommands/TclCommandOpenGerber.py diff --git a/FlatCAM.py b/FlatCAM.py index 1c1b1f7f..92ed2e1c 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 strenght 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 840cf425..dd917bb7 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from contextlib import contextmanager ######################################## ## Imports part of FlatCAM ## @@ -25,6 +26,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool +from xml.dom.minidom import parseString as parse_xml_string import tclCommands ######################################## @@ -103,6 +105,9 @@ class App(QtCore.QObject): # and is ready to be used. new_object_available = QtCore.pyqtSignal(object) + # Emmited when shell command is finished(one command only) + shell_command_finished = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): @@ -451,6 +456,7 @@ class App(QtCore.QObject): self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg) + self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg) self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject) self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas) self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True)) @@ -523,8 +529,8 @@ class App(QtCore.QObject): self.shell.resize(*self.defaults["shell_shape"]) self.shell.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() if self.cmd_line_shellfile: try: @@ -542,6 +548,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() @@ -676,12 +693,16 @@ class App(QtCore.QObject): 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 """ - return self.exec_command_test(text, False) + self.report_usage('exec_command') + + result = self.exec_command_test(text, False) + return result def exec_command_test(self, text, reraise=True): """ @@ -692,11 +713,10 @@ class App(QtCore.QObject): :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)) if result!='None': self.shell.append_output(result + '\n') @@ -708,6 +728,9 @@ class App(QtCore.QObject): #show error in console and just return or in test raise exception if reraise: raise e + finally: + self.shell.close_proccessing() + pass return result """ @@ -1491,6 +1514,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() @@ -1612,6 +1638,53 @@ class App(QtCore.QObject): # thread safe. The new_project() self.open_project(filename) + def on_file_exportsvg(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportsvg") + App.log.debug("on_file_exportsvg()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("WARNING: No object selected.") + msg = "Please Select a Geometry object to export" + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + # Check for more compatible types and add as required + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob) + and not isinstance(obj, FlatCAMExcellon)): + msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + name = self.collection.get_active().options["name"] + + try: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG", + directory=self.get_last_folder(), filter="*.svg") + except TypeError: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG") + + filename = str(filename) + + if str(filename) == "": + self.inform.emit("Export SVG cancelled.") + return + else: + self.export_svg(name, filename) + def on_file_importsvg(self): """ Callback for menu item File->Import SVG. @@ -1696,6 +1769,51 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) + + def export_svg(self, obj_name, filename, scale_factor=0.00): + """ + Exports a Geometry Object to a SVG File + + :param filename: Path to the SVG file to save to. + :param outname: + :return: + """ + self.log.debug("export_svg()") + + try: + obj = self.collection.get_by_name(str(obj_name)) + except: + return "Could not retrieve object: %s" % obj_name + + with self.proc_container.new("Exporting SVG") as proc: + exported_svg = obj.export_svg(scale_factor=scale_factor) + + # Determine bounding area for svg export + bounds = obj.bounds() + size = obj.size() + + # Convert everything to strings for use in the xml doc + svgwidth = str(size[0]) + svgheight = str(size[1]) + minx = str(bounds[0]) + miny = str(bounds[1] - size[1]) + uom = obj.units.lower() + + # Add a SVG Header and footer to the svg output from shapely + # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape + svg_header = '' + svg_header += '' + svg_footer = ' ' + svg_elem = svg_header + exported_svg + svg_footer + + # Parse the xml through a xml parser just to add line feeds and to make it look more pretty for the output + doc = parse_xml_string(svg_elem) + with open(filename, 'w') as fp: + fp.write(doc.toprettyxml()) + def import_svg(self, filename, outname=None): """ Adds a new Geometry Object to the projects and populates @@ -2079,7 +2197,7 @@ class App(QtCore.QObject): return a, kwa - from contextlib import contextmanager + @contextmanager def wait_signal(signal, timeout=10000): """Block loop until signal emitted, or timeout (ms) elapses.""" @@ -2094,30 +2212,40 @@ class App(QtCore.QObject): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raiseTclError(str(ex[0])) if status['timed_out']: raise Exception('Timed out!') - def wait_signal2(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" - loop = QtCore.QEventLoop() - signal.connect(loop.quit) - status = {'timed_out': False} - - def report_quit(): - status['timed_out'] = True - loop.quit() - - if timeout is not None: - QtCore.QTimer.singleShot(timeout, report_quit) - loop.exec_() - - if status['timed_out']: - raise Exception('Timed out!') + # def wait_signal2(signal, timeout=10000): + # """Block loop until signal emitted, or timeout (ms) elapses.""" + # loop = QtCore.QEventLoop() + # signal.connect(loop.quit) + # status = {'timed_out': False} + # + # def report_quit(): + # status['timed_out'] = True + # loop.quit() + # + # if timeout is not None: + # QtCore.QTimer.singleShot(timeout, report_quit) + # loop.exec_() + # + # if status['timed_out']: + # raise Exception('Timed out!') def mytest(*args): to = int(args[0]) @@ -2142,8 +2270,60 @@ class App(QtCore.QObject): except Exception as e: return str(e) + def mytest2(*args): + to = int(args[0]) + + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break + + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") + + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() + return str(self.collection.get_names()) + def mytest3(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + self.inform.emit("mytest3") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def mytest4(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + 1/0 # Force exception + self.inform.emit("mytest4") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def export_svg(name, filename, *args): + a, kwa = h(*args) + types = {'scale_factor': float} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + self.export_svg(str(name), str(filename), **kwa) + def import_svg(filename, *args): a, kwa = h(*args) types = {'outname': str} @@ -3274,6 +3454,18 @@ class App(QtCore.QObject): 'fcn': mytest, 'help': "Test function. Only for testing." }, + 'mytest2': { + 'fcn': mytest2, + 'help': "Test function. Only for testing." + }, + 'mytest3': { + 'fcn': mytest3, + 'help': "Test function. Only for testing." + }, + 'mytest4': { + 'fcn': mytest4, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." @@ -3284,6 +3476,14 @@ class App(QtCore.QObject): "> import_svg " + " filename: Path to the file to import." }, + 'export_svg': { + 'fcn': export_svg, + 'help': "Export a Geometry Object as a SVG File\n" + + "> export_svg [-scale_factor <0.0 (float)>]\n" + + " name: Name of the geometry object to export.\n" + + " filename: Path to the file to export.\n" + + " scale_factor: Multiplication factor used for scaling line widths during export." + }, 'open_gerber': { 'fcn': open_gerber, 'help': "Opens a Gerber file.\n" diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 8bb2445e..3c01d124 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -48,6 +48,10 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) self.menufile.addAction(self.menufileimportsvg) + # Export SVG ... + self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self) + self.menufile.addAction(self.menufileexportsvg) + # Save Project self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self) self.menufile.addAction(self.menufilesaveproject) diff --git a/FlatCAMShell.py b/FlatCAMShell.py index 695d7a9b..c85e86e0 100644 --- a/FlatCAMShell.py +++ b/FlatCAMShell.py @@ -22,4 +22,4 @@ class FCShell(termwidget.TermWidget): return True def child_exec_command(self, text): - self._sysShell.exec_command(text) + self._sysShell.exec_command(text) \ No newline at end of file diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 528171b6..8e51a7fa 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -1,6 +1,4 @@ from PyQt4 import QtCore -#import FlatCAMApp - class Worker(QtCore.QObject): """ @@ -8,12 +6,33 @@ class Worker(QtCore.QObject): in a single independent thread. """ + # avoid multiple tests for debug availability + pydef_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.pydef_failed: + try: + import pydevd + pydevd.settrace(suspend=False, trace_only_current_thread=True) + except ImportError: + pass + def run(self): + + # allow debuging/breakpoints in this threads + #pydevd.settrace(suspend=False, trace_only_current_thread=True) + # FlatCAMApp.App.log.debug("Worker Started!") self.app.log.debug("Worker Started!") @@ -21,9 +40,12 @@ class Worker(QtCore.QObject): self.app.worker_task.connect(self.do_worker_task) def do_worker_task(self, task): + # FlatCAMApp.App.log.debug("Running task: %s" % str(task)) self.app.log.debug("Running task: %s" % str(task)) + self.allow_debug() + # 'worker_name' property of task allows to target # specific worker. if 'worker_name' in task and task['worker_name'] == self.name: @@ -35,4 +57,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 5a8487a3..2a717ea6 100644 --- a/camlib.py +++ b/camlib.py @@ -890,6 +890,26 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] + def export_svg(self, scale_factor=0.00): + """ + Exports the Gemoetry Object as a SVG Element + + :return: SVG Element + """ + # Make sure we see a Shapely Geometry class and not a list + geom = cascaded_union(self.flatten()) + + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + + # If 0 or less which is invalid then default to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor <= 0: + scale_factor = 0.05 + + # Convert to a SVG + svg_elem = geom.svg(scale_factor=scale_factor) + return svg_elem class ApertureMacro: """ @@ -3334,6 +3354,54 @@ class CNCjob(Geometry): self.create_geometry() + def export_svg(self, scale_factor=0.00): + """ + Exports the CNC Job as a SVG Element + + :scale_factor: float + :return: SVG Element string + """ + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + # If not specified then try and use the tool diameter + # This way what is on screen will match what is outputed for the svg + # This is quite a useful feature for svg's used with visicut + + if scale_factor <= 0: + scale_factor = self.options['tooldia'] / 2 + + # If still 0 then defailt to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor == 0: + scale_factor = 0.05 + + # Seperate the list of cuts and travels into 2 distinct lists + # This way we can add different formatting / colors to both + cuts = [] + travels = [] + for g in self.gcode_parsed: + if g['kind'][0] == 'C': cuts.append(g) + if g['kind'][0] == 'T': travels.append(g) + + # Used to determine the overall board size + self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + + # Convert the cuts and travels into single geometry objects we can render as svg xml + if travels: + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + if cuts: + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + + # Render the SVG Xml + # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set + # It's better to have the travels sitting underneath the cuts for visicut + svg_elem = "" + if travels: + svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D") + if cuts: + svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF") + + return svg_elem # def get_bounds(geometry_set): # xmin = Inf diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 7f8a7e8d..a446a15b 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,3 +1,4 @@ +import sys import re import FlatCAMApp import abc @@ -41,6 +42,9 @@ class TclCommand(object): 'examples': [] } + # original incoming arguments into command + original_args = None + def __init__(self, app): self.app = app if self.app is None: @@ -59,6 +63,18 @@ class TclCommand(object): 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. @@ -176,7 +192,7 @@ class TclCommand(object): # check options for key in options: - if key not in self.option_types: + if key not in self.option_types and key is not 'timeout': self.raise_tcl_error('Unknown parameter: %s' % key) try: named_args[key] = self.option_types[key](options[key]) @@ -201,8 +217,11 @@ class TclCommand(object): :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: @@ -239,9 +258,15 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 30 sec, but it can be much more - default_timeout = 30000 + # default timeout for operation is 300 sec, but it can be much more + default_timeout = 300000 + output = None + + def execute_call(self, args, unnamed_args): + + self.output = self.execute(args, unnamed_args) + self.app.shell_command_finished.emit(self) def execute_wrapper(self, *args): """ @@ -254,7 +279,7 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=30000): + def wait_signal(signal, timeout=300000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() signal.connect(loop.quit) @@ -267,27 +292,43 @@ class TclCommandSignaled(TclCommand): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raise_tcl_error(str(ex[0])) + if status['timed_out']: self.app.raise_tcl_unknown_error('Operation timed out!') try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) + self.original_args=args args, unnamed_args = self.check_args(args) if 'timeout' in args: passed_timeout=args['timeout'] del args['timeout'] else: passed_timeout=self.default_timeout - with wait_signal(self.app.new_object_available, passed_timeout): + + # set detail for processing, it will be there until next open or close + self.app.shell.open_proccessing(self.get_current_command()) + + with wait_signal(self.app.shell_command_finished, passed_timeout): # every TclCommandNewObject ancestor support timeout as parameter, # but it does not mean anything for child itself # when operation will be really long is good to set it higher then defqault 30s - return self.execute(args, unnamed_args) + self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py index 6d2c2afd..c9e35078 100644 --- a/tclCommands/TclCommandAddPolygon.py +++ b/tclCommands/TclCommandAddPolygon.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandAddPolygon(TclCommand.TclCommand): +class TclCommandAddPolygon(TclCommand.TclCommandSignaled): """ Tcl shell command to create a polygon in the given Geometry object """ diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py index 57b8fe02..3c994760 100644 --- a/tclCommands/TclCommandAddPolyline.py +++ b/tclCommands/TclCommandAddPolyline.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandAddPolyline(TclCommand.TclCommand): +class TclCommandAddPolyline(TclCommand.TclCommandSignaled): """ Tcl shell command to create a polyline in the given Geometry object """ diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index 17a677ee..e088d0ec 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandCncjob(TclCommand.TclCommand): +class TclCommandCncjob(TclCommand.TclCommandSignaled): """ Tcl shell command to Generates a CNC Job from a Geometry Object. @@ -70,11 +70,6 @@ class TclCommandCncjob(TclCommand.TclCommand): if 'outname' not in args: args['outname'] = name + "_cnc" - 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) diff --git a/tclCommands/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py index 520f6ecb..feecd870 100644 --- a/tclCommands/TclCommandExportGcode.py +++ b/tclCommands/TclCommandExportGcode.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandExportGcode(TclCommand.TclCommand): +class TclCommandExportGcode(TclCommand.TclCommandSignaled): """ Tcl shell command to export gcode as tcl output for "set X [export_gcode ...]" diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py index 16f2fee4..ac69e7cb 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandExteriors(TclCommand.TclCommand): +class TclCommandExteriors(TclCommand.TclCommandSignaled): """ Tcl shell command to get exteriors of polygons """ @@ -57,7 +57,7 @@ class TclCommandExteriors(TclCommand.TclCommand): if not isinstance(obj, Geometry): self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj): + def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_exteriors() diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index 2314be3f..61bfe9f0 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandInteriors(TclCommand.TclCommand): +class TclCommandInteriors(TclCommand.TclCommandSignaled): """ Tcl shell command to get interiors of polygons """ @@ -57,7 +57,7 @@ class TclCommandInteriors(TclCommand.TclCommand): if not isinstance(obj, Geometry): self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj): + def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_interiors() 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 index 3055dbc7..af67a9cd 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -2,12 +2,16 @@ import pkgutil import sys # allowed command modules -import tclCommands.TclCommandExteriors -import tclCommands.TclCommandInteriors import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline -import tclCommands.TclCommandExportGcode import tclCommands.TclCommandCncjob +import tclCommands.TclCommandExportGcode +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors +import tclCommands.TclCommandIsolate +import tclCommands.TclCommandNew +import tclCommands.TclCommandOpenGerber + __all__ = [] diff --git a/termwidget.py b/termwidget.py index d6309fd3..94bbb80f 100644 --- a/termwidget.py +++ b/termwidget.py @@ -4,7 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history. """ import cgi - +from PyQt4 import QtCore from PyQt4.QtCore import pyqtSignal from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ QSizePolicy, QTextCursor, QTextEdit, \ @@ -113,6 +113,32 @@ 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 + :return: + """ + + self._edit.setTextColor(QtCore.Qt.white) + self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen) + if detail is None: + self._edit.setPlainText("...proccessing...") + else: + self._edit.setPlainText("...proccessing... [%s]" % detail) + + self._edit.setDisabled(True) + + def close_proccessing(self): + """ + Close processing and enable using shell commands again + :return: + """ + + self._edit.setTextColor(QtCore.Qt.black) + self._edit.setTextBackgroundColor(QtCore.Qt.white) + self._edit.setPlainText('') + self._edit.setDisabled(False) + def _append_to_browser(self, style, text): """ Convert text to HTML for inserting it to browser diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 3d75f6aa..526354fb 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -8,7 +8,7 @@ from time import sleep import os import tempfile -class TclShellCommandTest(unittest.TestCase): +class TclShellTest(unittest.TestCase): gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' @@ -30,12 +30,21 @@ class TclShellCommandTest(unittest.TestCase): # user-defined defaults). self.fc = App(user_defaults=False) + self.fc.shell.show() + pass + def tearDown(self): + 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') @@ -46,6 +55,7 @@ class TclShellCommandTest(unittest.TestCase): 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 @@ -139,9 +149,21 @@ class TclShellCommandTest(unittest.TestCase): # 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), From cac2f74be2788c71fdb76088415d326b70c07bb7 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 24 Mar 2016 23:23:27 +0100 Subject: [PATCH 09/22] fix pydevd_failed typo and it was not reset to True --- FlatCAMWorker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 8e51a7fa..90edaa7f 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -7,7 +7,7 @@ class Worker(QtCore.QObject): """ # avoid multiple tests for debug availability - pydef_failed = False + pydevd_failed = False def __init__(self, app, name=None): super(Worker, self).__init__() @@ -21,12 +21,12 @@ class Worker(QtCore.QObject): :return: """ - if not self.pydef_failed: + if not self.pydevd_failed: try: import pydevd pydevd.settrace(suspend=False, trace_only_current_thread=True) except ImportError: - pass + self.pydevd_failed=True def run(self): From 2082446ab04b6e173d491cd7efe4984039fd6915 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 00:59:02 +0100 Subject: [PATCH 10/22] tweak signal handling --- FlatCAMApp.py | 4 ++++ FlatCAMWorker.py | 22 ++++++++++++++++----- tclCommands/TclCommand.py | 41 ++++++++++++++++++++++++++++++--------- tests/test_tcl_shell.py | 26 ++++++++++++++++--------- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index dd917bb7..0ed5b896 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -108,6 +108,10 @@ class App(QtCore.QObject): # 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) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 90edaa7f..a1f49a81 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -48,12 +48,24 @@ class Worker(QtCore.QObject): # 'worker_name' property of task allows to target # specific worker. - if 'worker_name' in task and task['worker_name'] == self.name: - task['fcn'](*task['params']) - return + #if 'worker_name' in task and task['worker_name'] == self.name: + # task['fcn'](*task['params']) + # return + + #if 'worker_name' not in task and self.name is None: + # task['fcn'](*task['params']) + # return + + + if ('worker_name' in task and task['worker_name'] == self.name) or \ + ('worker_name' not in task and self.name is None): + + try: + task['fcn'](*task['params']) + except Exception as e: + self.app.thread_exception.emit(e) + raise e - if 'worker_name' not in task and self.name is None: - task['fcn'](*task['params']) return # FlatCAMApp.App.log.debug("Task ignored.") diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index a446a15b..f713b319 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -258,15 +258,17 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300 sec, but it can be much more - default_timeout = 300000 + # default timeout for operation is 10 sec, but it can be much more + default_timeout = 10000 output = None def execute_call(self, args, unnamed_args): - self.output = self.execute(args, unnamed_args) - self.app.shell_command_finished.emit(self) + try: + self.output = self.execute(args, unnamed_args) + finally: + self.app.shell_command_finished.emit(self) def execute_wrapper(self, *args): """ @@ -279,11 +281,16 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=300000): + def wait_signal(signal, timeout=10000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.app.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -292,18 +299,23 @@ class TclCommandSignaled(TclCommand): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + # Block loop.exec_() + # Restore exception management sys.excepthook = oeh if ex: self.raise_tcl_error(str(ex[0])) @@ -324,12 +336,23 @@ class TclCommandSignaled(TclCommand): # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) + self.output = None + + def handle_finished(obj): + self.app.shell_command_finished.disconnect(handle_finished) + # TODO: handle output + pass + + self.app.shell_command_finished.connect(handle_finished) + with wait_signal(self.app.shell_command_finished, passed_timeout): # every TclCommandNewObject ancestor support timeout as parameter, # but it does not mean anything for child itself # when operation will be really long is good to set it higher then defqault 30s self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) + return self.output + except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 526354fb..ecf60f56 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -1,6 +1,8 @@ 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 @@ -10,6 +12,8 @@ import tempfile class TclShellTest(unittest.TestCase): + setup = False + gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' @@ -24,20 +28,23 @@ class TclShellTest(unittest.TestCase): drill_diameter = 0.8 def setUp(self): - self.app = QtGui.QApplication(sys.argv) - # Create App, keep app defaults (do not load - # user-defined defaults). - self.fc = App(user_defaults=False) + if not self.setup: + self.setup=True + self.app = QtGui.QApplication(sys.argv) - self.fc.shell.show() + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + self.fc.shell.show() pass def tearDown(self): - self.app.closeAllWindows() - - del self.fc - del self.app + #self.fc.tcl=None + #self.app.closeAllWindows() + #del self.fc + #del self.app pass def test_set_get_units(self): @@ -60,6 +67,7 @@ class TclShellTest(unittest.TestCase): # open gerber files top, bottom and cutout + self.fc.exec_command_test('set_sys units MM') self.fc.exec_command_test('new') From f61aa397d4b577bcbd5e2517ff6c0138756793a1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 11:12:43 +0100 Subject: [PATCH 11/22] fix test hanging for shell --- tests/test_tcl_shell.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index ecf60f56..5813c230 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -12,8 +12,6 @@ import tempfile class TclShellTest(unittest.TestCase): - setup = False - gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' @@ -27,24 +25,22 @@ class TclShellTest(unittest.TestCase): cutout_diameter = 3 drill_diameter = 0.8 - def setUp(self): + @classmethod + def setUpClass(self): - if not self.setup: - self.setup=True - self.app = QtGui.QApplication(sys.argv) + 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.shell.show() - # Create App, keep app defaults (do not load - # user-defined defaults). - self.fc = App(user_defaults=False) - - self.fc.shell.show() - pass - - def tearDown(self): - #self.fc.tcl=None - #self.app.closeAllWindows() - #del self.fc - #del self.app + @classmethod + def tearDownClass(self): + self.fc.tcl=None + self.app.closeAllWindows() + del self.fc + del self.app pass def test_set_get_units(self): From 80d6c657d5bd7ef5e45cdf312934a314a384190f Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 13:56:18 +0100 Subject: [PATCH 12/22] merge changes from master merge dockable shell --- FlatCAMApp.py | 61 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0ed5b896..d17bbbcf 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from xml.dom.minidom import parseString as parse_xml_string from contextlib import contextmanager ######################################## @@ -26,7 +27,6 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool -from xml.dom.minidom import parseString as parse_xml_string import tclCommands ######################################## @@ -105,6 +105,8 @@ class App(QtCore.QObject): # and is ready to be used. new_object_available = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) + # Emmited when shell command is finished(one command only) shell_command_finished = QtCore.pyqtSignal(object) @@ -112,8 +114,6 @@ class App(QtCore.QObject): # in the worker task. thread_exception = QtCore.pyqtSignal(object) - message = QtCore.pyqtSignal(str, str, str) - def __init__(self, user_defaults=True, post_gui=None): """ Starts the application. @@ -479,7 +479,7 @@ class App(QtCore.QObject): self.ui.menuviewdisableall.triggered.connect(self.disable_plots) self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True)) self.ui.menuviewenable.triggered.connect(self.enable_all_plots) - self.ui.menutoolshell.triggered.connect(lambda: self.shell.show()) + self.ui.menutoolshell.triggered.connect(self.on_toggle_shell) self.ui.menuhelp_about.triggered.connect(self.on_about) self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url)) self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url)) @@ -493,7 +493,7 @@ class App(QtCore.QObject): self.ui.editgeo_btn.triggered.connect(self.edit_geometry) self.ui.updategeo_btn.triggered.connect(self.editor2geometry) self.ui.delete_btn.triggered.connect(self.on_delete) - self.ui.shell_btn.triggered.connect(lambda: self.shell.show()) + self.ui.shell_btn.triggered.connect(self.on_toggle_shell) # Object list self.collection.view.activated.connect(self.on_row_activated) # Options @@ -528,14 +528,24 @@ class App(QtCore.QObject): self.shell = FCShell(self) self.shell.setWindowIcon(self.ui.app_icon) self.shell.setWindowTitle("FlatCAM Shell") - if self.defaults["shell_at_startup"]: - self.shell.show() 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.init_tcl() + self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") + self.ui.shell_dock.setWidget(self.shell) + self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.ui.shell_dock.setFeatures(QtGui.QDockWidget.DockWidgetMovable | + QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetClosable) + self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) + + if self.defaults["shell_at_startup"]: + self.ui.shell_dock.show() + else: + self.ui.shell_dock.hide() + if self.cmd_line_shellfile: try: with open(self.cmd_line_shellfile, "r") as myfile: @@ -1064,6 +1074,16 @@ class App(QtCore.QObject): if not silent: self.inform.emit("Defaults saved.") + def on_toggle_shell(self): + """ + toggle shell if is visible close it if closed open it + :return: + """ + if self.ui.shell_dock.isVisible(): + self.ui.shell_dock.hide() + else: + self.ui.shell_dock.show() + def on_edit_join(self): """ Callback for Edit->Join. Joins the selected geometry objects into @@ -2201,13 +2221,22 @@ class App(QtCore.QObject): return a, kwa - @contextmanager def wait_signal(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" + """ + Block loop until signal emitted, timeout (ms) elapses + or unhandled exception happens in a thread. + + :param signal: Signal to wait for. + """ loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -2216,17 +2245,23 @@ class App(QtCore.QObject): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + #### Block #### loop.exec_() + + # Restore exception management sys.excepthook = oeh if ex: self.raiseTclError(str(ex[0])) From b333d136b5fd114b516a9a6f0e70babe9dd16196 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 31 Mar 2016 17:16:14 +0200 Subject: [PATCH 13/22] merge changes from master cleanups and prepare for pull request --- FlatCAM.py | 2 +- FlatCAMShell.py | 2 +- FlatCAMWorker.py | 12 +------- termwidget.py | 80 ++++++++++++++++++++---------------------------- 4 files changed, 36 insertions(+), 60 deletions(-) diff --git a/FlatCAM.py b/FlatCAM.py index 92ed2e1c..1cb60c9f 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -12,7 +12,7 @@ def debug_trace(): debug_trace() -# all X11 calling should be thread safe otherwise we have strenght issues +# all X11 calling should be thread safe otherwise we have strange issues QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) app = QtGui.QApplication(sys.argv) diff --git a/FlatCAMShell.py b/FlatCAMShell.py index c85e86e0..695d7a9b 100644 --- a/FlatCAMShell.py +++ b/FlatCAMShell.py @@ -22,4 +22,4 @@ class FCShell(termwidget.TermWidget): return True def child_exec_command(self, text): - self._sysShell.exec_command(text) \ No newline at end of file + self._sysShell.exec_command(text) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index a1f49a81..cbe9de91 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -1,5 +1,6 @@ from PyQt4 import QtCore + class Worker(QtCore.QObject): """ Implements a queue of tasks to be carried out in order @@ -46,17 +47,6 @@ class Worker(QtCore.QObject): self.allow_debug() - # 'worker_name' property of task allows to target - # specific worker. - #if 'worker_name' in task and task['worker_name'] == self.name: - # task['fcn'](*task['params']) - # return - - #if 'worker_name' not in task and self.name is None: - # task['fcn'](*task['params']) - # return - - if ('worker_name' in task and task['worker_name'] == self.name) or \ ('worker_name' not in task and self.name is None): diff --git a/termwidget.py b/termwidget.py index 94bbb80f..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 import QtCore -from PyQt4.QtCore import pyqtSignal +from PyQt4.QtCore import pyqtSignal, Qt from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ QSizePolicy, QTextCursor, QTextEdit, \ QVBoxLayout, QWidget @@ -19,13 +18,13 @@ class _ExpandableTextEdit(QTextEdit): historyNext = pyqtSignal() historyPrev = pyqtSignal() - def __init__(self, termWidget, *args): + def __init__(self, termwidget, *args): QTextEdit.__init__(self, *args) self.setStyleSheet("font: 9pt \"Courier\";") self._fittedHeight = 1 self.textChanged.connect(self._fit_to_document) self._fit_to_document() - self._termWidget = termWidget + self._termWidget = termwidget def sizeHint(self): """ @@ -39,10 +38,10 @@ class _ExpandableTextEdit(QTextEdit): """ Update widget height to fit all text """ - documentSize = self.document().size().toSize() - self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height()) + documentsize = self.document().size().toSize() + self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height()) self.setMaximumHeight(self._fittedHeight) - self.updateGeometry(); + self.updateGeometry() def keyPressEvent(self, event): """ @@ -55,30 +54,33 @@ class _ExpandableTextEdit(QTextEdit): return elif event.matches(QKeySequence.MoveToNextLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeEnd = text[cursorPos:] + cursor_pos = self.textCursor().position() + textBeforeEnd = text[cursor_pos:] # if len(textBeforeEnd.splitlines()) <= 1: if len(textBeforeEnd.split('\n')) <= 1: self.historyNext.emit() return elif event.matches(QKeySequence.MoveToPreviousLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeStart = text[:cursorPos] + cursor_pos = self.textCursor().position() + text_before_start = text[:cursor_pos] # lineCount = len(textBeforeStart.splitlines()) - lineCount = len(textBeforeStart.split('\n')) - if len(textBeforeStart) > 0 and \ - (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'): - lineCount += 1 - if lineCount <= 1: + line_count = len(text_before_start.split('\n')) + if len(text_before_start) > 0 and \ + (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'): + line_count += 1 + if line_count <= 1: self.historyPrev.emit() return elif event.matches(QKeySequence.MoveToNextPage) or \ - event.matches(QKeySequence.MoveToPreviousPage): + event.matches(QKeySequence.MoveToPreviousPage): return self._termWidget.browser().keyPressEvent(event) QTextEdit.keyPressEvent(self, event) + def insertFromMimeData(self, mime_data): + # Paste only plain text. + self.insertPlainText(mime_data.text()) class TermWidget(QWidget): """ @@ -94,8 +96,9 @@ class TermWidget(QWidget): self._browser = QTextEdit(self) self._browser.setStyleSheet("font: 9pt \"Courier\";") self._browser.setReadOnly(True) - self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() + - "span {white-space:pre;}") + self._browser.document().setDefaultStyleSheet( + self._browser.document().defaultStyleSheet() + + "span {white-space:pre;}") self._edit = _ExpandableTextEdit(self, self) self._edit.historyNext.connect(self._on_history_next) @@ -116,11 +119,13 @@ class TermWidget(QWidget): def open_proccessing(self, detail=None): """ Open processing and disable using shell commands again until all commands are finished - :return: + + :param detail: text detail about what is currently called from TCL to python + :return: None """ - self._edit.setTextColor(QtCore.Qt.white) - self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen) + self._edit.setTextColor(Qt.white) + self._edit.setTextBackgroundColor(Qt.darkGreen) if detail is None: self._edit.setPlainText("...proccessing...") else: @@ -134,8 +139,8 @@ class TermWidget(QWidget): :return: """ - self._edit.setTextColor(QtCore.Qt.black) - self._edit.setTextBackgroundColor(QtCore.Qt.white) + self._edit.setTextColor(Qt.black) + self._edit.setTextBackgroundColor(Qt.white) self._edit.setPlainText('') self._edit.setDisabled(False) @@ -146,30 +151,12 @@ class TermWidget(QWidget): assert style in ('in', 'out', 'err') text = cgi.escape(text) - text = text.replace('\n', '
') - if style != 'out': - def_bg = self._browser.palette().color(QPalette.Base) - h, s, v, a = def_bg.getHsvF() - - if style == 'in': - if v > 0.5: # white background - v = v - (v / 8) # make darker - else: - v = v + ((1 - v) / 4) # make ligher - else: # err - if v < 0.5: - v = v + ((1 - v) / 4) # make ligher - - if h == -1: # make red - h = 0 - s = .4 - else: - h = h + ((1 - h) * 0.5) # make more red - - bg = QColor.fromHsvF(h, s, v).name() - text = '%s' % (str(bg), text) + if style == 'in': + text = '%s' % text + elif style == 'err': + text = '%s' % text else: text = '%s' % text # without span
is ignored!!! @@ -264,4 +251,3 @@ class TermWidget(QWidget): self._historyIndex -= 1 self._edit.setPlainText(self._history[self._historyIndex]) self._edit.moveCursor(QTextCursor.End) - From e941e55a4abda91fe7b3fe9c52115afbc4749ab6 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 31 Mar 2016 17:29:11 +0200 Subject: [PATCH 14/22] show ui.shell_dock instead of shell during tests --- tests/test_tcl_shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 5813c230..d36f30ed 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -33,7 +33,7 @@ class TclShellTest(unittest.TestCase): # Create App, keep app defaults (do not load # user-defined defaults). self.fc = App(user_defaults=False) - self.fc.shell.show() + self.fc.ui.shell_dock.show() @classmethod def tearDownClass(self): From a4845d150e5e031e373b8670bb6572c977f12b48 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 10:43:06 +0200 Subject: [PATCH 15/22] add important comment --- FlatCAMApp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d31a7044..32b501e5 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -685,6 +685,7 @@ class App(QtCore.QObject): 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: """ From b98954dccd383f178b1a6b58e4e491281e4045c1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:20:50 +0200 Subject: [PATCH 16/22] fix error handling in signaled commands, error gets info about different scoup instead of true error more detaild error print including python trace when more complex unknown error reinplement drillcncjob fix camlib problem with all drills("all" was already there) but it crashes on tools without points, when no tools "all" is as default add timeout to all helps if command is signaled --- FlatCAMApp.py | 28 +++++++++- camlib.py | 36 +++++++------ tclCommands/TclCommand.py | 57 ++++++++++++++++---- tclCommands/TclCommandCncjob.py | 3 +- tclCommands/TclCommandDrillcncjob.py | 81 ++++++++++++++++++++++++++++ tclCommands/__init__.py | 4 +- 6 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 tclCommands/TclCommandDrillcncjob.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 32b501e5..020ec593 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1,4 +1,4 @@ -import sys +import sys, traceback import urllib import getopt import random @@ -695,6 +695,30 @@ class App(QtCore.QObject): 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 + 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=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 @@ -702,7 +726,7 @@ class App(QtCore.QObject): :return: raise exception """ - self.tcl.eval('return -code error "%s"' % text) + self.display_tcl_error(text) raise self.TclErrorException(text) def exec_command(self, text): diff --git a/camlib.py b/camlib.py index 2a717ea6..0d1bd383 100644 --- a/camlib.py +++ b/camlib.py @@ -2818,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/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index f713b319..57e4a9d1 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -125,6 +125,10 @@ class TclCommand(object): 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)) @@ -192,10 +196,13 @@ class TclCommand(object): # check options for key in options: - if key not in self.option_types and key is not 'timeout': + if key not in self.option_types and key != 'timeout': self.raise_tcl_error('Unknown parameter: %s' % key) try: - named_args[key] = self.option_types[key](options[key]) + 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))) @@ -207,6 +214,31 @@ class TclCommand(object): 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. @@ -225,8 +257,10 @@ class TclCommand(object): 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.raise_tcl_unknown_error(unknown) + self.app.display_tcl_error(unknown, error_info) + self.raise_tcl_unknown_error(unknown) @abc.abstractmethod def execute(self, args, unnamed_args): @@ -242,7 +276,6 @@ class TclCommand(object): raise NotImplementedError("Please Implement this method") - class TclCommandSignaled(TclCommand): """ !!! I left it here only for demonstration !!! @@ -266,7 +299,13 @@ class TclCommandSignaled(TclCommand): 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) @@ -336,12 +375,12 @@ class TclCommandSignaled(TclCommand): # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) - self.output = None - def handle_finished(obj): self.app.shell_command_finished.disconnect(handle_finished) - # TODO: handle output - pass + if self.error is not None: + self.log.error("TCL command '%s' failed." % str(self)) + self.app.display_tcl_error(self.error, self.error_info) + self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished) @@ -355,4 +394,4 @@ class TclCommandSignaled(TclCommand): except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) - self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file + self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index e088d0ec..e6d84de3 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -49,8 +49,7 @@ class TclCommandCncjob(TclCommand.TclCommandSignaled): ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), ('multidepth', 'Use or not multidepth cnccut.'), ('depthperpass', 'Height of one layer for multidepth.'), - ('outname', 'Name of the resulting Geometry object.'), - ('timeout', 'Max wait for job timeout before error.') + ('outname', 'Name of the resulting Geometry object.') ]), 'examples': [] } diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py new file mode 100644 index 00000000..17441967 --- /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: 1).'), + ('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", name, job_init) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index af67a9cd..2f733017 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -1,10 +1,11 @@ import pkgutil import sys -# allowed command modules +# 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 @@ -19,7 +20,6 @@ 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. From c2cdaaf45234029ae43cc4a1b438258fad6decac Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:37:40 +0200 Subject: [PATCH 17/22] fix display also for nonsignaled exceptions in execute_wrapper --- tclCommands/TclCommand.py | 2 ++ tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 57e4a9d1..24f8295f 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -393,5 +393,7 @@ class TclCommandSignaled(TclCommand): 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/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index 17441967..f65931fd 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -39,7 +39,7 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): ('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: 1).'), + ('toolchange', 'Enable tool changes (example: True).'), ('outname', 'Name of the resulting Geometry object.') ]), 'examples': [] From 5bd6432ead16b96dfe40a15134765d37b5364d45 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 6 Apr 2016 11:20:53 +0200 Subject: [PATCH 18/22] solve message in special tcl keywords used in wrong context as "unknown" --- FlatCAMApp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec593..b2ee9745 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,6 +762,11 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") + + # solve message in special tcl keywords used in wrong context as "unknown" + if e.message == 'invalid command name ""': + result=result.replace('""','"%s"' % text.replace("\n","")) + self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 4c20040fbe792cf247f2412e841d5e3aa1ecd99c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 9 Apr 2016 12:48:32 +0200 Subject: [PATCH 19/22] fix errors in tool selection --- camlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camlib.py b/camlib.py index 0d1bd383..7c0cd11a 100644 --- a/camlib.py +++ b/camlib.py @@ -2777,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 ',' @@ -2819,7 +2819,7 @@ class CNCjob(Geometry): for tool in tools: # only if tool have some points, otherwise thre may be error and this part is useless - if "tool" in points: + if tool in points: # Tool change sequence (optional) if toolchange: gcode += "G00 Z%.4f\n" % toolchangez From fae9875dd896762c156dc18b1e38985f136ec639 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:09:26 +0200 Subject: [PATCH 20/22] remove unknown workaround --- FlatCAMApp.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index b2ee9745..020ec593 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,11 +762,6 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") - - # solve message in special tcl keywords used in wrong context as "unknown" - if e.message == 'invalid command name ""': - result=result.replace('""','"%s"' % text.replace("\n","")) - self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 26a8b7347b2474ae5bb287091ddff2d2aa8db873 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:10:25 +0200 Subject: [PATCH 21/22] change default timeout fix outname bug in drillcncjob --- tclCommands/TclCommand.py | 4 ++-- tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 24f8295f..bc7cd2c2 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,8 +291,8 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 10 sec, but it can be much more - default_timeout = 10000 + # default timeout for operation is 300000 sec, but it can be much more + default_timeout = 300000 output = None diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index f65931fd..783b6599 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -78,4 +78,4 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): job_obj.gcode_parse() job_obj.create_geometry() - self.app.new_object("cncjob", name, job_init) + self.app.new_object("cncjob", args['outname'], job_init) From e236a60be90e9ca9bdc4be90ca6c4731dcdf8265 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 15:14:18 +0200 Subject: [PATCH 22/22] implement system values background_timeout and verbose_error_level implement correct error level handling based on verbose_error_level , fix double print of tcl error and do not wrap unknown exceptions into TCL known --- FlatCAMApp.py | 27 +++++++++++++++++++-------- tclCommands/TclCommand.py | 13 ++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec593..872a6e4f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -286,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, @@ -679,7 +681,6 @@ class App(QtCore.QObject): """ 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): @@ -704,14 +705,24 @@ class App(QtCore.QObject): """ if isinstance(error, Exception): + exc_type, exc_value, exc_traceback = error_info - 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)) + 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 diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index bc7cd2c2..b93ec752 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,9 +291,6 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300000 sec, but it can be much more - default_timeout = 300000 - output = None def execute_call(self, args, unnamed_args): @@ -320,7 +317,7 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=10000): + def wait_signal(signal, timeout=300000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() @@ -357,10 +354,10 @@ class TclCommandSignaled(TclCommand): # Restore exception management sys.excepthook = oeh if ex: - self.raise_tcl_error(str(ex[0])) + raise ex[0] if status['timed_out']: - self.app.raise_tcl_unknown_error('Operation 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__)) @@ -370,7 +367,7 @@ class TclCommandSignaled(TclCommand): passed_timeout=args['timeout'] del args['timeout'] else: - passed_timeout=self.default_timeout + 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()) @@ -378,8 +375,6 @@ class TclCommandSignaled(TclCommand): def handle_finished(obj): self.app.shell_command_finished.disconnect(handle_finished) if self.error is not None: - self.log.error("TCL command '%s' failed." % str(self)) - self.app.display_tcl_error(self.error, self.error_info) self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished)