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