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