diff --git a/DblSidedTool.py b/DblSidedTool.py
index 93e579a..a6ce803 100644
--- a/DblSidedTool.py
+++ b/DblSidedTool.py
@@ -169,8 +169,10 @@ class DblSidedTool(FlatCAMTool):
# For now, lets limit to Gerbers and Excellons.
# assert isinstance(gerb, FlatCAMGerber)
- if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
- self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
+ if not isinstance(fcobj, FlatCAMGerber) and \
+ not isinstance(fcobj, FlatCAMExcellon) and \
+ not isinstance(fcobj, FlatCAMGeometry):
+ self.info("ERROR: Only Gerber, Excellon and Geometry objects can be mirrored.")
return
axis = self.mirror_axis.get_value()
diff --git a/FlatCAM.py b/FlatCAM.py
index 1c1b1f7..46cfc30 100644
--- a/FlatCAM.py
+++ b/FlatCAM.py
@@ -2,14 +2,23 @@ import sys
from PyQt4 import QtGui
from FlatCAMApp import App
+
def debug_trace():
- '''Set a tracepoint in the Python debugger that works with Qt'''
+ """
+ Set a tracepoint in the Python debugger that works with Qt
+ :return: None
+ """
from PyQt4.QtCore import pyqtRemoveInputHook
#from pdb import set_trace
pyqtRemoveInputHook()
#set_trace()
debug_trace()
+
+# All X11 calling should be thread safe otherwise we have strange issues
+# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
+# NOTE: Never talk to the GUI from threads! This is why I commented the above.
+
app = QtGui.QApplication(sys.argv)
fc = App()
-sys.exit(app.exec_())
\ No newline at end of file
+sys.exit(app.exec_())
diff --git a/FlatCAMApp.py b/FlatCAMApp.py
index ee8a0e3..62757ef 100644
--- a/FlatCAMApp.py
+++ b/FlatCAMApp.py
@@ -1,4 +1,4 @@
-import sys
+import sys, traceback
import urllib
import getopt
import random
@@ -10,6 +10,8 @@ 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
########################################
## Imports part of FlatCAM ##
@@ -25,6 +27,7 @@ from FlatCAMDraw import FlatCAMDraw
from FlatCAMProcess import *
from MeasurementTool import Measurement
from DblSidedTool import DblSidedTool
+import tclCommands
########################################
@@ -72,11 +75,26 @@ class App(QtCore.QObject):
## Manual URL
manual_url = "http://flatcam.org/manual/index.html"
- ## Signals
- inform = QtCore.pyqtSignal(str) # Message
- worker_task = QtCore.pyqtSignal(dict) # Worker task
+ ##################
+ ## Signals ##
+ ##################
+
+ # Inform the user
+ # Handled by:
+ # * App.info() --> Print on the status bar
+ inform = QtCore.pyqtSignal(str)
+
+ # General purpose background task
+ worker_task = QtCore.pyqtSignal(dict)
+
+ # File opened
+ # Handled by:
+ # * register_folder()
+ # * register_recent()
file_opened = QtCore.pyqtSignal(str, str) # File type and filename
+
progress = QtCore.pyqtSignal(int) # Percentage of progress
+
plots_updated = QtCore.pyqtSignal()
# Emitted by new_object() and passes the new object as argument.
@@ -87,9 +105,15 @@ class App(QtCore.QObject):
# Emitted when a new object has been added to the collection
# and is ready to be used.
new_object_available = QtCore.pyqtSignal(object)
+
message = QtCore.pyqtSignal(str, str, str)
- block_plots = False
+ # 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)
def __init__(self, user_defaults=True, post_gui=None):
"""
@@ -148,11 +172,6 @@ class App(QtCore.QObject):
self.app_home = os.path.dirname(os.path.realpath(__file__))
App.log.debug("Application path is " + self.app_home)
App.log.debug("Started in " + os.getcwd())
-
- # cx_freeze workaround
- if os.path.isfile(self.app_home):
- self.app_home = os.path.dirname(self.app_home)
-
os.chdir(self.app_home)
####################
@@ -168,7 +187,7 @@ class App(QtCore.QObject):
#### Plot Area ####
# self.plotcanvas = PlotCanvas(self.ui.splitter)
- self.plotcanvas = PlotCanvas(self.ui.right_layout)
+ self.plotcanvas = PlotCanvas(self.ui.right_layout, self)
self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
@@ -197,9 +216,6 @@ class App(QtCore.QObject):
"gerber_isotooldia": self.defaults_form.gerber_group.iso_tool_dia_entry,
"gerber_isopasses": self.defaults_form.gerber_group.iso_width_entry,
"gerber_isooverlap": self.defaults_form.gerber_group.iso_overlap_entry,
- "gerber_ncctools": self.defaults_form.gerber_group.ncc_tool_dia_entry,
- "gerber_nccoverlap": self.defaults_form.gerber_group.ncc_overlap_entry,
- "gerber_nccmargin": self.defaults_form.gerber_group.ncc_margin_entry,
"gerber_combine_passes": self.defaults_form.gerber_group.combine_passes_cb,
"gerber_cutouttooldia": self.defaults_form.gerber_group.cutout_tooldia_entry,
"gerber_cutoutmargin": self.defaults_form.gerber_group.cutout_margin_entry,
@@ -215,6 +231,8 @@ class App(QtCore.QObject):
"excellon_travelz": self.defaults_form.excellon_group.travelz_entry,
"excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry,
"excellon_spindlespeed": self.defaults_form.excellon_group.spindlespeed_entry,
+ "excellon_toolchangez": self.defaults_form.excellon_group.toolchangez_entry,
+ "excellon_tooldia": self.defaults_form.excellon_group.tooldia_entry,
"geometry_plot": self.defaults_form.geometry_group.plot_cb,
"geometry_cutz": self.defaults_form.geometry_group.cutz_entry,
"geometry_travelz": self.defaults_form.geometry_group.travelz_entry,
@@ -227,7 +245,9 @@ class App(QtCore.QObject):
"cncjob_plot": self.defaults_form.cncjob_group.plot_cb,
"cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry,
"cncjob_prepend": self.defaults_form.cncjob_group.prepend_text,
- "cncjob_append": self.defaults_form.cncjob_group.append_text
+ "cncjob_append": self.defaults_form.cncjob_group.append_text,
+ "cncjob_dwell": self.defaults_form.cncjob_group.dwell_cb,
+ "cncjob_dwelltime": self.defaults_form.cncjob_group.dwelltime_cb
}
self.defaults = LoudDict()
@@ -242,9 +262,6 @@ class App(QtCore.QObject):
"gerber_isotooldia": 0.016,
"gerber_isopasses": 1,
"gerber_isooverlap": 0.15,
- "gerber_ncctools": "1.0, 0.5",
- "gerber_nccoverlap": 0.4,
- "gerber_nccmargin": 1,
"gerber_cutouttooldia": 0.07,
"gerber_cutoutmargin": 0.1,
"gerber_cutoutgapsize": 0.15,
@@ -259,6 +276,8 @@ class App(QtCore.QObject):
"excellon_travelz": 0.1,
"excellon_feedrate": 3.0,
"excellon_spindlespeed": None,
+ "excellon_toolchangez": 1.0,
+ "excellon_tooldia": 0.016,
"geometry_plot": True,
"geometry_cutz": -0.002,
"geometry_travelz": 0.1,
@@ -272,6 +291,13 @@ class App(QtCore.QObject):
"cncjob_tooldia": 0.016,
"cncjob_prepend": "",
"cncjob_append": "",
+ "cncjob_dwell": True,
+ "cncjob_dwelltime": 1,
+ "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,
@@ -320,7 +346,6 @@ class App(QtCore.QObject):
QtCore.QTimer.singleShot(self.defaults["defaults_save_period_ms"], auto_save_defaults)
self.options_form = GlobalOptionsUI()
-
self.options_form_fields = {
"units": self.options_form.units_radio,
"gerber_plot": self.options_form.gerber_group.plot_cb,
@@ -329,9 +354,6 @@ class App(QtCore.QObject):
"gerber_isotooldia": self.options_form.gerber_group.iso_tool_dia_entry,
"gerber_isopasses": self.options_form.gerber_group.iso_width_entry,
"gerber_isooverlap": self.options_form.gerber_group.iso_overlap_entry,
- "gerber_ncctools": self.options_form.gerber_group.ncc_tool_dia_entry,
- "gerber_nccoverlap": self.options_form.gerber_group.ncc_overlap_entry,
- "gerber_nccmargin": self.options_form.gerber_group.ncc_margin_entry,
"gerber_combine_passes": self.options_form.gerber_group.combine_passes_cb,
"gerber_cutouttooldia": self.options_form.gerber_group.cutout_tooldia_entry,
"gerber_cutoutmargin": self.options_form.gerber_group.cutout_margin_entry,
@@ -347,6 +369,8 @@ class App(QtCore.QObject):
"excellon_travelz": self.options_form.excellon_group.travelz_entry,
"excellon_feedrate": self.options_form.excellon_group.feedrate_entry,
"excellon_spindlespeed": self.options_form.excellon_group.spindlespeed_entry,
+ "excellon_toolchangez": self.options_form.excellon_group.toolchangez_entry,
+ "excellon_tooldia": self.options_form.excellon_group.tooldia_entry,
"geometry_plot": self.options_form.geometry_group.plot_cb,
"geometry_cutz": self.options_form.geometry_group.cutz_entry,
"geometry_travelz": self.options_form.geometry_group.travelz_entry,
@@ -372,9 +396,6 @@ class App(QtCore.QObject):
"gerber_isotooldia": 0.016,
"gerber_isopasses": 1,
"gerber_isooverlap": 0.15,
- "gerber_ncctools": "1.0, 0.5",
- "gerber_nccoverlap": 0.4,
- "gerber_nccmargin": 1,
"gerber_combine_passes": True,
"gerber_cutouttooldia": 0.07,
"gerber_cutoutmargin": 0.1,
@@ -390,6 +411,8 @@ class App(QtCore.QObject):
"excellon_travelz": 0.1,
"excellon_feedrate": 3.0,
"excellon_spindlespeed": None,
+ "excellon_toolchangez": 1.0,
+ "excellon_tooldia": 0.016,
"geometry_plot": True,
"geometry_cutz": -0.002,
"geometry_travelz": 0.1,
@@ -402,7 +425,9 @@ class App(QtCore.QObject):
"cncjob_plot": True,
"cncjob_tooldia": 0.016,
"cncjob_prepend": "",
- "cncjob_append": ""
+ "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).
})
self.options.update(self.defaults) # Copy app defaults to project options
#self.options_write_form()
@@ -412,10 +437,6 @@ class App(QtCore.QObject):
self.ui.project_tab_layout.addWidget(self.collection.view)
#### End of Data ####
- #### Adjust tabs width ####
- self.collection.view.setMinimumWidth(self.ui.options_scroll_area.widget().sizeHint().width() +
- self.ui.options_scroll_area.verticalScrollBar().sizeHint().width())
-
#### Worker ####
App.log.info("Starting Worker...")
self.worker = Worker(self)
@@ -436,7 +457,7 @@ class App(QtCore.QObject):
lambda: self.worker_task.emit({'fcn': self.version_check,
'params': [],
'worker_name': "worker2"}))
- # self.thr2.start()
+ self.thr2.start()
### Signal handling ###
## Custom signals
@@ -454,6 +475,8 @@ class App(QtCore.QObject):
self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon)
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))
@@ -472,7 +495,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))
@@ -486,7 +509,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
@@ -521,13 +544,23 @@ 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.tcl = Tkinter.Tcl()
- self.setup_shell()
+
+ self.init_tcl()
+
+ self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell")
+ self.ui.shell_dock.setWidget(self.shell)
+ 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:
@@ -545,6 +578,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()
@@ -647,23 +691,130 @@ class App(QtCore.QObject):
else:
self.defaults['stats'][resource] = 1
+ # TODO: This shouldn't be here.
+ class TclErrorException(Exception):
+ """
+ this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command
+ """
+ pass
+
+ def shell_message(self, msg, show=False, error=False):
+ """
+ Shows a message on the FlatCAM Shell
+
+ :param msg: Message to display.
+ :param show: Opens the shell.
+ :param error: Shows the message as an error.
+ :return: None
+ """
+ if show:
+ self.ui.shell_dock.show()
+
+ if error:
+ self.shell.append_error(msg + "\n")
+ else:
+ self.shell.append_output(msg + "\n")
+
+ def raise_tcl_unknown_error(self, unknownException):
+ """
+ Raise exception if is different type than TclErrorException
+ this is here mainly to show unknown errors inside TCL shell console.
+
+ :param unknownException:
+ :return:
+ """
+
+ if not isinstance(unknownException, self.TclErrorException):
+ self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+ else:
+ raise unknownException
+
+ def display_tcl_error(self, error, error_info=None):
+ """
+ escape bracket [ with \ otherwise there is error
+ "ERROR: missing close-bracket" instead of real error
+ :param error: it may be text or exception
+ :return: None
+ """
+
+ if isinstance(error, Exception):
+
+ exc_type, exc_value, exc_traceback = error_info
+ if not isinstance(error, self.TclErrorException):
+ show_trace = 1
+ else:
+ show_trace = int(self.defaults['verbose_error_level'])
+
+ if show_trace > 0:
+ trc = traceback.format_list(traceback.extract_tb(exc_traceback))
+ trc_formated = []
+ for a in reversed(trc):
+ trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
+ text = "%s\nPython traceback: %s\n%s" % (exc_value,
+ exc_type,
+ "\n".join(trc_formated))
+
+ else:
+ text = "%s" % error
+ else:
+ text = error
+
+ text = text.replace('[', '\\[').replace('"', '\\"')
+
+ self.tcl.eval('return -code error "%s"' % text)
+
+ def raise_tcl_error(self, text):
+ """
+ this method pass exception from python into TCL as error, so we get stacktrace and reason
+ :param text: text of error
+ :return: raise exception
+ """
+
+ self.display_tcl_error(text)
+ raise self.TclErrorException(text)
+
def exec_command(self, text):
"""
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+ Also handles execution in separated threads
+
+ :param text:
+ :return: output if there was any
+ """
+
+ self.report_usage('exec_command')
+
+ result = self.exec_command_test(text, False)
+ return result
+
+ def exec_command_test(self, text, reraise=True):
+ """
+ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
:param text: Input command
- :return: None
+ :param reraise: raise exception and not hide it, used mainly in unittests
+ :return: output if there was any
"""
- self.report_usage('exec_command')
text = str(text)
try:
+ self.shell.open_proccessing()
result = self.tcl.eval(str(text))
- self.shell.append_output(result + '\n')
+ if result != 'None':
+ self.shell.append_output(result + '\n')
except Tkinter.TclError, e:
- self.shell.append_error('ERROR: ' + str(e) + '\n')
- return
+ #this will display more precise answer if something in TCL shell fail
+ result = self.tcl.eval("set errorInfo")
+ self.log.error("Exec command Exception: %s" % (result + '\n'))
+ self.shell.append_error('ERROR: ' + result + '\n')
+ #show error in console and just return or in test raise exception
+ if reraise:
+ raise e
+ finally:
+ self.shell.close_proccessing()
+ pass
+ return result
"""
Code below is unsused. Saved for later.
@@ -704,16 +855,27 @@ class App(QtCore.QObject):
def info(self, msg):
"""
- Writes on the status bar.
+ Informs the user. Normally on the status bar, optionally
+ also on the shell.
:param msg: Text to write.
+ :param toshell: Forward the
:return: None
"""
+
+ # Type of message in brackets at the begining of the message.
match = re.search("\[([^\]]+)\](.*)", msg)
if match:
- self.ui.fcinfo.set_status(QtCore.QString(match.group(2)), level=match.group(1))
+ level = match.group(1)
+ msg_ = match.group(2)
+ self.ui.fcinfo.set_status(QtCore.QString(msg_), level=level)
+
+ error = level == "error" or level == "warning"
+ self.shell_message(msg, error=error, show=True)
+
else:
self.ui.fcinfo.set_status(QtCore.QString(msg), level="info")
+ self.shell_message(msg)
def load_defaults(self):
"""
@@ -837,8 +999,7 @@ class App(QtCore.QObject):
obj = classdict[kind](name)
obj.units = self.options["units"] # TODO: The constructor should look at defaults.
- # Set options from "Project options" form
- self.options_read_form()
+ # Set default options from self.options
for option in self.options:
if option.find(kind + "_") == 0:
oname = option[len(kind) + 1:]
@@ -993,6 +1154,17 @@ 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
@@ -1000,6 +1172,7 @@ class App(QtCore.QObject):
:return: None
"""
+
objs = self.collection.get_selected()
def initialize(obj, app):
@@ -1154,7 +1327,7 @@ class App(QtCore.QObject):
# Options to scale
dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
- 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
+ 'excellon_travelz', 'excellon_feedrate', 'excellon_toolchangez', 'excellon_tooldia', 'cncjob_tooldia',
'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
'geometry_paintmargin']
@@ -1267,7 +1440,7 @@ class App(QtCore.QObject):
return
# Remove plot
- self.plotcanvas.offscreen_figure.delaxes(self.collection.get_active().axes)
+ self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
self.plotcanvas.auto_adjust_axes()
# Clear form
@@ -1307,9 +1480,7 @@ class App(QtCore.QObject):
self.plot_all()
def on_row_activated(self, index):
- if index.isValid():
- if index.internalPointer().parent_item != self.collection.root_item:
- self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
+ self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
def on_object_created(self, obj):
"""
@@ -1326,13 +1497,8 @@ class App(QtCore.QObject):
self.inform.emit("Object (%s) created: %s" % (obj.kind, obj.options['name']))
self.new_object_available.emit(obj)
- if not self.block_plots:
- obj.plot()
-
- # Fit on first added object only
- if len(self.collection.get_list()) == 1:
- self.on_zoom_fit(None)
-
+ obj.plot()
+ self.on_zoom_fit(None)
t1 = time.time() # DEBUG
self.log.debug("%f seconds adding object and plotting." % (t1 - t0))
@@ -1454,6 +1620,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()
@@ -1575,6 +1744,76 @@ 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.
+
+ :return: None
+ """
+ self.report_usage("on_file_importsvg")
+ App.log.debug("on_file_importsvg()")
+
+ try:
+ filename = QtGui.QFileDialog.getOpenFileName(caption="Import SVG",
+ directory=self.get_last_folder())
+ except TypeError:
+ filename = QtGui.QFileDialog.getOpenFileName(caption="Import SVG")
+
+ filename = str(filename)
+
+ if str(filename) == "":
+ self.inform.emit("Open cancelled.")
+ else:
+ self.worker_task.emit({'fcn': self.import_svg,
+ 'params': [filename]})
+
def on_file_saveproject(self):
"""
Callback for menu item File->Save Project. Saves the project to
@@ -1610,9 +1849,6 @@ class App(QtCore.QObject):
except TypeError:
filename = QtGui.QFileDialog.getSaveFileName(caption="Save Project As ...")
- if filename == "":
- return
-
try:
f = open(filename, 'r')
f.close()
@@ -1639,6 +1875,81 @@ 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 an SVG file.
+
+ :param filename: Path to the SVG file to save to.
+ :return:
+ """
+
+ self.log.debug("export_svg()")
+
+ try:
+ obj = self.collection.get_by_name(str(obj_name))
+ except:
+ # TODO: The return behavior has not been established... should raise exception?
+ 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
+ it with shapes extracted from the SVG file.
+
+ :param filename: Path to the SVG file.
+ :param outname:
+ :return:
+ """
+
+ def obj_init(geo_obj, app_obj):
+
+ geo_obj.import_svg(filename)
+
+ with self.proc_container.new("Importing SVG") as proc:
+
+ # Object name
+ name = outname or filename.split('/')[-1].split('\\')[-1]
+
+ self.new_object("geometry", name, obj_init)
+
+ # Register recent file
+ self.file_opened.emit("svg", filename)
+
+ # GUI feedback
+ self.inform.emit("Opened: " + filename)
+
def open_gerber(self, filename, follow=False, outname=None):
"""
Opens a Gerber file, parses it and creates a new object for
@@ -1674,6 +1985,17 @@ class App(QtCore.QObject):
self.log.error(str(e))
raise
+ except:
+ msg = "[error] An internal error has ocurred. See shell.\n"
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ raise
+
+ if gerber_obj.is_empty():
+ app_obj.inform.emit("[error] No geometry found in file: " + filename)
+ self.collection.set_active(gerber_obj.options["name"])
+ self.collection.delete_active()
+
# Further parsing
self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here
@@ -1718,18 +2040,31 @@ class App(QtCore.QObject):
try:
excellon_obj.parse_file(filename)
+
except IOError:
app_obj.inform.emit("[error] Cannot open file: " + filename)
self.progress.emit(0) # TODO: self and app_bjj mixed
raise IOError("Cannot open file: " + filename)
+ except:
+ msg = "[error] An internal error has ocurred. See shell.\n"
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ raise
+
try:
excellon_obj.create_geometry()
- except Exception as e:
- app_obj.inform.emit("[error] Failed to create geometry after parsing: " + filename)
- self.progress.emit(0)
- raise e
+ except:
+ msg = "[error] An internal error has ocurred. See shell.\n"
+ msg += traceback.format_exc()
+ app_obj.inform.emit(msg)
+ raise
+
+ if excellon_obj.is_empty():
+ app_obj.inform.emit("[error] No geometry found in file: " + filename)
+ self.collection.set_active(excellon_obj.options["name"])
+ self.collection.delete_active()
#self.progress.emit(70)
with self.proc_container.new("Opening Excellon."):
@@ -1827,7 +2162,6 @@ class App(QtCore.QObject):
:return: None
"""
App.log.debug("Opening project: " + filename)
- self.block_plots = True
## Open and parse
try:
@@ -1868,7 +2202,6 @@ class App(QtCore.QObject):
self.plot_all()
self.inform.emit("Project loaded from: " + filename)
App.log.debug("Project loaded")
- self.block_plots = False
def propagate_defaults(self):
"""
@@ -1959,7 +2292,8 @@ class App(QtCore.QObject):
def shelp(p=None):
if not p:
- return "Available commands:\n" + '\n'.join([' ' + cmd for cmd in commands]) + \
+ return "Available commands:\n" + \
+ '\n'.join([' ' + cmd for cmd in sorted(commands)]) + \
"\n\nType help for usage.\n Example: help open_gerber"
if p not in commands:
@@ -1996,6 +2330,159 @@ class App(QtCore.QObject):
return a, kwa
+ @contextmanager
+ def wait_signal(signal, timeout=10000):
+ """
+ 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():
+ status['timed_out'] = True
+ loop.quit()
+
+ yield
+
+ # Temporarily change how exceptions are managed.
+ oeh = sys.excepthook
+ ex = []
+
+ def except_hook(type_, value, traceback_):
+ ex.append(value)
+ oeh(type_, value, traceback_)
+ sys.excepthook = except_hook
+
+ # Terminate on timeout
+ if timeout is not None:
+ QtCore.QTimer.singleShot(timeout, report_quit)
+
+ #### Block ####
+ loop.exec_()
+
+ # Restore exception management
+ sys.excepthook = oeh
+ if ex:
+ self.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 mytest(*args):
+ to = int(args[0])
+
+ try:
+ 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):
+ iso.generatecncjob()
+ # iso.generatecncjob()
+ # wait_signal2(self.new_object_available, to)
+
+ return str(self.collection.get_names())
+
+ 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}
+
+ for key in kwa:
+ if key not in types:
+ return 'Unknown parameter: %s' % key
+ kwa[key] = types[key](kwa[key])
+
+ self.import_svg(str(filename), **kwa)
+
def open_gerber(filename, *args):
a, kwa = h(*args)
types = {'follow': bool,
@@ -2048,8 +2535,8 @@ class App(QtCore.QObject):
return "Could not retrieve object: %s" % name
def geo_init_me(geo_obj, app_obj):
- margin = kwa['margin']+kwa['dia']/2
- gap_size = kwa['dia']+kwa['gapsize']
+ margin = kwa['margin'] + kwa['dia'] / 2
+ gap_size = kwa['dia'] + kwa['gapsize']
minx, miny, maxx, maxy = obj.bounds()
minx -= margin
maxx += margin
@@ -2088,6 +2575,97 @@ class App(QtCore.QObject):
return 'Ok'
+ def geocutout(name=None, *args):
+ """
+ TCL shell command - see help section
+
+ Subtract gaps from geometry, this will not create new object
+
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ """
+
+ try:
+ a, kwa = h(*args)
+ types = {'dia': float,
+ 'gapsize': float,
+ 'gaps': str}
+
+ # How gaps wil be rendered:
+ # lr - left + right
+ # tb - top + bottom
+ # 4 - left + right +top + bottom
+ # 2lr - 2*left + 2*right
+ # 2tb - 2*top + 2*bottom
+ # 8 - 2*left + 2*right +2*top + 2*bottom
+
+ if name is None:
+ self.raise_tcl_error('Argument name is missing.')
+
+ for key in kwa:
+ if key not in types:
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key])))
+
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+ # Get min and max data for each object as we just cut rectangles across X or Y
+ xmin, ymin, xmax, ymax = obj.bounds()
+ px = 0.5 * (xmin + xmax)
+ py = 0.5 * (ymin + ymax)
+ lenghtx = (xmax - xmin)
+ lenghty = (ymax - ymin)
+ gapsize = kwa['gapsize'] + kwa['dia'] / 2
+
+ if kwa['gaps'] == '8' or kwa['gaps'] == '2lr':
+
+ subtract_rectangle(name,
+ xmin - gapsize,
+ py - gapsize + lenghty / 4,
+ xmax + gapsize,
+ py + gapsize + lenghty / 4)
+ subtract_rectangle(name,
+ xmin-gapsize,
+ py - gapsize - lenghty / 4,
+ xmax + gapsize,
+ py + gapsize - lenghty / 4)
+
+ if kwa['gaps'] == '8' or kwa['gaps']=='2tb':
+ subtract_rectangle(name,
+ px - gapsize + lenghtx / 4,
+ ymin-gapsize,
+ px + gapsize + lenghtx / 4,
+ ymax + gapsize)
+ subtract_rectangle(name,
+ px - gapsize - lenghtx / 4,
+ ymin - gapsize,
+ px + gapsize - lenghtx / 4,
+ ymax + gapsize)
+
+ if kwa['gaps'] == '4' or kwa['gaps']=='lr':
+ subtract_rectangle(name,
+ xmin - gapsize,
+ py - gapsize,
+ xmax + gapsize,
+ py + gapsize)
+
+ if kwa['gaps'] == '4' or kwa['gaps']=='tb':
+ subtract_rectangle(name,
+ px - gapsize,
+ ymin - gapsize,
+ px + gapsize,
+ ymax + gapsize)
+
+ except Exception as unknown:
+ self.raise_tcl_unknown_error(unknown)
+
def mirror(name, *args):
a, kwa = h(*args)
types = {'box': str,
@@ -2108,9 +2686,10 @@ class App(QtCore.QObject):
if obj is None:
return "Object not found: %s" % name
- if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon):
- return "ERROR: Only Gerber and Excellon objects can be mirrored."
-
+ if not isinstance(obj, FlatCAMGerber) and \
+ not isinstance(obj, FlatCAMExcellon) and \
+ not isinstance(obj, FlatCAMGeometry):
+ return "ERROR: Only Gerber, Excellon and Geometry objects can be mirrored."
# Axis
try:
@@ -2118,7 +2697,6 @@ class App(QtCore.QObject):
except KeyError:
return "ERROR: Specify -axis X or -axis Y"
-
# Box
if 'box' in kwa:
try:
@@ -2156,22 +2734,81 @@ class App(QtCore.QObject):
return 'Ok'
- def drillcncjob(name, *args):
+ def aligndrillgrid(outname, *args):
a, kwa = h(*args)
- types = {'tools': str,
- 'outname': str,
- 'drillz': float,
- 'travelz': float,
- 'feedrate': float,
- 'spindlespeed': int,
- 'toolchange': int
+ types = {'gridx': float,
+ 'gridy': float,
+ 'gridoffsetx': float,
+ 'gridoffsety': float,
+ 'columns':int,
+ 'rows':int,
+ 'dia': float
}
+ for key in kwa:
+ if key not in types:
+ return 'Unknown parameter: %s' % key
+ kwa[key] = types[key](kwa[key])
+
+ if 'columns' not in kwa or 'rows' not in kwa:
+ return "ERROR: Specify -columns and -rows"
+
+ if 'gridx' not in kwa or 'gridy' not in kwa:
+ return "ERROR: Specify -gridx and -gridy"
+
+ if 'dia' not in kwa:
+ return "ERROR: Specify -dia"
+
+ if 'gridoffsetx' not in kwa:
+ gridoffsetx=0
+ else:
+ gridoffsetx=kwa['gridoffsetx']
+
+ if 'gridoffsety' not in kwa:
+ gridoffsety=0
+ else:
+ gridoffsety=kwa['gridoffsety']
+
+ # Tools
+ tools = {"1": {"C": kwa['dia']}}
+
+ def aligndrillgrid_init_me(init_obj, app_obj):
+ drills = []
+ currenty=0
+
+ for row in range(kwa['rows']):
+ currentx=0
+
+ for col in range(kwa['columns']):
+ point = Point(currentx + gridoffsetx, currenty + gridoffsety)
+ drills.append({"point": point, "tool": "1"})
+ currentx = currentx + kwa['gridx']
+
+ currenty = currenty + kwa['gridy']
+
+ init_obj.tools = tools
+ init_obj.drills = drills
+ init_obj.create_geometry()
+
+ self.new_object("excellon", outname , aligndrillgrid_init_me)
+
+ def aligndrill(name, *args):
+ a, kwa = h(*args)
+ types = {'box': str,
+ 'axis': str,
+ 'holes': str,
+ 'grid': float,
+ 'minoffset': float,
+ 'gridoffset': float,
+ 'axisoffset': float,
+ 'dia': float,
+ 'dist': float}
for key in kwa:
if key not in types:
return 'Unknown parameter: %s' % key
kwa[key] = types[key](kwa[key])
+ # Get source object.
try:
obj = self.collection.get_by_name(str(name))
except:
@@ -2180,177 +2817,382 @@ class App(QtCore.QObject):
if obj is None:
return "Object not found: %s" % name
- if not isinstance(obj, FlatCAMExcellon):
- return "ERROR: Only Excellon objects can be drilled."
+ if not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon):
+ return "ERROR: Only Gerber, Geometry and Excellon objects can be used."
+ # Axis
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):
- assert isinstance(job_obj, FlatCAMCNCjob), \
- "Initializer expected FlatCAMCNCjob, got %s" % type(job_obj)
+ axis = kwa['axis'].upper()
+ except KeyError:
+ return "ERROR: Specify -axis X or -axis Y"
+
+ if not ('holes' in kwa or ('grid' in kwa and 'gridoffset' in kwa)):
+ return "ERROR: Specify -holes or -grid with -gridoffset "
+
+ if 'holes' in kwa:
+ try:
+ holes = eval("[" + kwa['holes'] + "]")
+ except KeyError:
+ return "ERROR: Wrong -holes format (X1,Y1),(X2,Y2)"
+
+ xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+ # Tools
+ tools = {"1": {"C": kwa['dia']}}
+
+ def alligndrill_init_me(init_obj, app_obj):
+
+ drills = []
+ if 'holes' in kwa:
+ for hole in holes:
+ point = Point(hole)
+ point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+ drills.append({"point": point, "tool": "1"})
+ drills.append({"point": point_mirror, "tool": "1"})
+ else:
+ if not 'box' in kwa:
+ return "ERROR: -grid can be used only for -box"
+
+ if 'axisoffset' in kwa:
+ axisoffset=kwa['axisoffset']
+ else:
+ axisoffset=0
+
+ # This will align hole to given aligngridoffset and minimal offset from pcb, based on selected axis
+ if axis == "X":
+ firstpoint = kwa['gridoffset']
+
+ while (xmin - kwa['minoffset']) < firstpoint:
+ firstpoint = firstpoint - kwa['grid']
+
+ lastpoint = kwa['gridoffset']
+
+ while (xmax + kwa['minoffset']) > lastpoint:
+ lastpoint = lastpoint + kwa['grid']
+
+ localHoles = (firstpoint, axisoffset), (lastpoint, axisoffset)
- 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)
+ else:
+ firstpoint = kwa['gridoffset']
+
+ while (ymin - kwa['minoffset']) < firstpoint:
+ firstpoint = firstpoint - kwa['grid']
+
+ lastpoint = kwa['gridoffset']
+
+ while (ymax + kwa['minoffset']) > lastpoint:
+ lastpoint=lastpoint+kwa['grid']
+
+ localHoles = (axisoffset, firstpoint), (axisoffset, lastpoint)
- except Exception, e:
- return "Operation failed: %s" % str(e)
+ for hole in localHoles:
+ point = Point(hole)
+ point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+ drills.append({"point": point, "tool": "1"})
+ drills.append({"point": point_mirror, "tool": "1"})
- return 'Ok'
+ init_obj.tools = tools
+ init_obj.drills = drills
+ init_obj.create_geometry()
- def drillmillgeometry(name, *args):
- a, kwa = h(*args)
- types = {'tooldia': float,
- 'tools': str,
- 'outname': str}
+ # Box
+ if 'box' in kwa:
+ try:
+ box = self.collection.get_by_name(kwa['box'])
+ except:
+ return "Could not retrieve object box: %s" % kwa['box']
- for key in kwa:
- if key not in types:
- return 'Unknown parameter: %s' % key
- kwa[key] = types[key](kwa[key])
+ if box is None:
+ return "Object box not found: %s" % kwa['box']
- try:
- if 'tools' in kwa:
- kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")]
- except Exception as e:
- return "Bad tools: %s" % str(e)
+ try:
+ xmin, ymin, xmax, ymax = box.bounds()
+ px = 0.5 * (xmin + xmax)
+ py = 0.5 * (ymin + ymax)
- try:
- obj = self.collection.get_by_name(str(name))
- except:
- return "Could not retrieve object: %s" % name
+ obj.app.new_object("excellon", name + "_aligndrill", alligndrill_init_me)
- if obj is None:
- return "Object not found: %s" % name
+ except Exception, e:
+ return "Operation failed: %s" % str(e)
- assert isinstance(obj, FlatCAMExcellon), \
- "Expected a FlatCAMExcellon object, got %s" % type(obj)
-
- try:
- success, msg = obj.generate_milling(**kwa)
- except Exception as e:
- return "Operation failed: %s" % str(e)
-
- if not success:
- return msg
-
- return 'Ok'
-
- def exteriors(obj_name, *args):
- a, kwa = h(*args)
- types = {'outname': str}
-
- for key in kwa:
- if key not in types:
- return 'Unknown parameter: %s' % key
- kwa[key] = types[key](kwa[key])
-
- try:
- obj = self.collection.get_by_name(str(obj_name))
- except:
- return "Could not retrieve object: %s" % obj_name
-
- if obj is None:
- return "Object not found: %s" % obj_name
-
- assert isinstance(obj, Geometry), \
- "Expected a Geometry, got %s" % type(obj)
-
- obj_exteriors = obj.get_exteriors()
-
- def geo_init(geo_obj, app_obj):
- geo_obj.solid_geometry = obj_exteriors
-
- if 'outname' in kwa:
- outname = kwa['outname']
else:
- outname = obj_name + ".exteriors"
+ try:
+ dist = float(kwa['dist'])
+ except KeyError:
+ dist = 0.0
+ except ValueError:
+ return "Invalid distance: %s" % kwa['dist']
- try:
- self.new_object('geometry', outname, geo_init)
- except Exception as e:
- return "Failed: %s" % str(e)
+ try:
+ px=dist
+ py=dist
+ obj.app.new_object("excellon", name + "_alligndrill", alligndrill_init_me)
+ except Exception, e:
+ return "Operation failed: %s" % str(e)
return 'Ok'
- def interiors(obj_name, *args):
- a, kwa = h(*args)
- types = {}
-
- for key in kwa:
- if key not in types:
- return 'Unknown parameter: %s' % key
- kwa[key] = types[key](kwa[key])
+ def drillcncjob(name=None, *args):
+ '''
+ TCL shell command - see help section
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ '''
try:
- obj = self.collection.get_by_name(str(obj_name))
- except:
- return "Could not retrieve object: %s" % obj_name
+ a, kwa = h(*args)
+ types = {'tools': str,
+ 'outname': str,
+ 'drillz': float,
+ 'travelz': float,
+ 'feedrate': float,
+ 'spindlespeed': int,
+ 'toolchange': int
+ }
- if obj is None:
- return "Object not found: %s" % obj_name
+ if name is None:
+ self.raise_tcl_error('Argument name is missing.')
- assert isinstance(obj, Geometry), \
- "Expected a Geometry, got %s" % type(obj)
+ for key in kwa:
+ if key not in types:
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key])))
- obj_interiors = obj.get_interiors()
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
- def geo_init(geo_obj, app_obj):
- geo_obj.solid_geometry = obj_interiors
+ if obj is None:
+ self.raise_tcl_error('Object not found: %s' % name)
- if 'outname' in kwa:
- outname = kwa['outname']
- else:
- outname = obj_name + ".interiors"
+ if not isinstance(obj, FlatCAMExcellon):
+ self.raise_tcl_error('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj)))
+
+ try:
+ # Get the tools from the list
+ job_name = kwa["outname"]
+
+ # Object initialization function for app.new_object()
+ def job_init(job_obj, app_obj):
+ job_obj.z_cut = kwa["drillz"]
+ job_obj.z_move = kwa["travelz"]
+ job_obj.feedrate = kwa["feedrate"]
+ job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None
+ toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False
+ job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange)
+ job_obj.gcode_parse()
+ job_obj.create_geometry()
+
+ obj.app.new_object("cncjob", job_name, job_init)
+
+ except Exception, e:
+ self.raise_tcl_error("Operation failed: %s" % str(e))
+
+ except Exception as unknown:
+ self.raise_tcl_unknown_error(unknown)
+
+ def millholes(name=None, *args):
+ '''
+ TCL shell command - see help section
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ '''
try:
- self.new_object('geometry', outname, geo_init)
- except Exception as e:
- return "Failed: %s" % str(e)
+ a, kwa = h(*args)
+ types = {'tooldia': float,
+ 'tools': str,
+ 'outname': str}
- return 'Ok'
+ if name is None:
+ self.raise_tcl_error('Argument name is missing.')
- def isolate(name, *args):
+ for key in kwa:
+ if key not in types:
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
+
+ try:
+ if 'tools' in kwa:
+ kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")]
+ except Exception as e:
+ self.raise_tcl_error("Bad tools: %s" % str(e))
+
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, FlatCAMExcellon):
+ self.raise_tcl_error('Only Excellon objects can be mill-drilled, got %s %s.' % (name, type(obj)))
+
+ try:
+ # This runs in the background: Block until done.
+ with wait_signal(self.new_object_available):
+ success, msg = obj.generate_milling(**kwa)
+
+ except Exception as e:
+ self.raise_tcl_error("Operation failed: %s" % str(e))
+
+ if not success:
+ self.raise_tcl_error(msg)
+
+ except Exception as unknown:
+ self.raise_tcl_unknown_error(unknown)
+
+ def exteriors(name=None, *args):
+ '''
+ TCL shell command - see help section
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ '''
+
+ try:
+ a, kwa = h(*args)
+ types = {'outname': str}
+
+ if name is None:
+ self.raise_tcl_error('Argument name is missing.')
+
+ for key in kwa:
+ if key not in types:
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
+
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ def geo_init(geo_obj, app_obj):
+ geo_obj.solid_geometry = obj_exteriors
+
+ if 'outname' in kwa:
+ outname = kwa['outname']
+ else:
+ outname = name + ".exteriors"
+
+ try:
+ obj_exteriors = obj.get_exteriors()
+ self.new_object('geometry', outname, geo_init)
+ except Exception as e:
+ self.raise_tcl_error("Failed: %s" % str(e))
+
+ except Exception as unknown:
+ self.raise_tcl_unknown_error(unknown)
+
+ def interiors(name=None, *args):
+ '''
+ TCL shell command - see help section
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ '''
+
+ try:
+ a, kwa = h(*args)
+ types = {'outname': str}
+
+ for key in kwa:
+ if key not in types:
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
+
+ if name is None:
+ self.raise_tcl_error('Argument name is missing.')
+
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ def geo_init(geo_obj, app_obj):
+ geo_obj.solid_geometry = obj_interiors
+
+ 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.raise_tcl_error("Failed: %s" % str(e))
+
+ except Exception as unknown:
+ self.raise_tcl_unknown_error(unknown)
+
+ def isolate(name=None, *args):
+ '''
+ TCL shell command - see help section
+ :param name: name of object
+ :param args: array of arguments
+ :return: "Ok" if completed without errors
+ '''
a, kwa = h(*args)
types = {'dia': float,
'passes': int,
'overlap': float,
- 'outname': str,
+ 'outname': str,
'combine': int}
for key in kwa:
if key not in types:
- return 'Unknown parameter: %s' % key
- kwa[key] = types[key](kwa[key])
-
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ kwa[key] = types[key](kwa[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key]))
try:
obj = self.collection.get_by_name(str(name))
except:
- return "Could not retrieve object: %s" % name
+ self.raise_tcl_error("Could not retrieve object: %s" % name)
if obj is None:
- return "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.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj)))
+
try:
obj.isolate(**kwa)
except Exception, e:
- return "Operation failed: %s" % str(e)
+ self.raise_tcl_error("Operation failed: %s" % str(e))
return 'Ok'
@@ -2362,7 +3204,9 @@ class App(QtCore.QObject):
'feedrate': float,
'tooldia': float,
'outname': str,
- 'spindlespeed': int
+ 'spindlespeed': int,
+ 'multidepth' : bool,
+ 'depthperpass' : float
}
for key in kwa:
@@ -2446,6 +3290,28 @@ class App(QtCore.QObject):
return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y,
topright_x, topright_y, topright_x, botleft_y)
+ def subtract_poly(obj_name, *args):
+ if len(args) % 2 != 0:
+ return "Incomplete coordinate."
+
+ points = [[float(args[2*i]), float(args[2*i+1])] for i in range(len(args)/2)]
+
+ try:
+ obj = self.collection.get_by_name(str(obj_name))
+ except:
+ return "Could not retrieve object: %s" % obj_name
+ if obj is None:
+ return "Object not found: %s" % obj_name
+
+ obj.subtract_polygon(points)
+ obj.plot()
+
+ return "OK."
+
+ def subtract_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y):
+ return subtract_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y,
+ topright_x, topright_y, topright_x, botleft_y)
+
def add_circle(obj_name, center_x, center_y, radius):
try:
obj = self.collection.get_by_name(str(obj_name))
@@ -2464,6 +3330,8 @@ class App(QtCore.QObject):
def delete(obj_name):
try:
+ #deselect all to avoid delete selected object when run delete from shell
+ self.collection.set_all_inactive()
self.collection.set_active(str(obj_name))
self.on_delete()
except Exception, e:
@@ -2495,6 +3363,121 @@ class App(QtCore.QObject):
if objs is not None:
self.new_object("geometry", obj_name, initialize)
+ def join_excellons(obj_name, *obj_names):
+ objs = []
+ for obj_n in obj_names:
+ obj = self.collection.get_by_name(str(obj_n))
+ if obj is None:
+ return "Object not found: %s" % obj_n
+ else:
+ objs.append(obj)
+
+ def initialize(obj, app):
+ FlatCAMExcellon.merge(objs, obj)
+
+ if objs is not None:
+ self.new_object("excellon", obj_name, initialize)
+
+ def panelize(name, *args):
+ a, kwa = h(*args)
+ types = {'box': str,
+ 'spacing_columns': float,
+ 'spacing_rows': float,
+ 'columns': int,
+ 'rows': int,
+ 'outname': str}
+
+ for key in kwa:
+ if key not in types:
+ return 'Unknown parameter: %s' % key
+ kwa[key] = types[key](kwa[key])
+
+ # Get source object.
+ try:
+ obj = self.collection.get_by_name(str(name))
+ except:
+ return "Could not retrieve object: %s" % name
+
+ if obj is None:
+ return "Object not found: %s" % name
+
+ if 'box' in kwa:
+ boxname=kwa['box']
+ try:
+ box = self.collection.get_by_name(boxname)
+ except:
+ return "Could not retrieve object: %s" % name
+ else:
+ box=obj
+
+ if 'columns' not in kwa or 'rows' not in kwa:
+ return "ERROR: Specify -columns and -rows"
+
+ if 'outname' in kwa:
+ outname=kwa['outname']
+ else:
+ outname=name+'_panelized'
+
+ if 'spacing_columns' in kwa:
+ spacing_columns=kwa['spacing_columns']
+ else:
+ spacing_columns=5
+
+ if 'spacing_rows' in kwa:
+ spacing_rows=kwa['spacing_rows']
+ else:
+ spacing_rows=5
+
+ xmin, ymin, xmax, ymax = box.bounds()
+ lenghtx = xmax-xmin+spacing_columns
+ lenghty = ymax-ymin+spacing_rows
+
+ currenty=0
+ def initialize_local(obj_init, app):
+ obj_init.solid_geometry = obj.solid_geometry
+ obj_init.offset([float(currentx), float(currenty)]),
+
+ def initialize_local_excellon(obj_init, app):
+ FlatCAMExcellon.merge(obj, obj_init)
+ obj_init.offset([float(currentx), float(currenty)]),
+
+ def initialize_geometry(obj_init, app):
+ FlatCAMGeometry.merge(objs, obj_init)
+
+ def initialize_excellon(obj_init, app):
+ FlatCAMExcellon.merge(objs, obj_init)
+
+ objs=[]
+ if obj is not None:
+
+ for row in range(kwa['rows']):
+ currentx=0
+ for col in range(kwa['columns']):
+ local_outname=outname+".tmp."+str(col)+"."+str(row)
+ if isinstance(obj, FlatCAMExcellon):
+ new_obj=self.new_object("excellon", local_outname, initialize_local_excellon)
+ else:
+ new_obj=self.new_object("geometry", local_outname, initialize_local)
+ objs.append(new_obj)
+ currentx=currentx+lenghtx
+ currenty=currenty+lenghty
+
+ if isinstance(obj, FlatCAMExcellon):
+ self.new_object("excellon", outname, initialize_excellon)
+ else:
+ self.new_object("geometry", outname, initialize_geometry)
+
+ #deselect all to avoid delete selected object when run delete from shell
+ self.collection.set_all_inactive()
+ for delobj in objs:
+ self.collection.set_active(delobj.options['name'])
+ self.on_delete()
+
+ else:
+ return "ERROR: obj is None"
+
+ return "Ok"
+
def make_docs():
output = ''
import collections
@@ -2588,15 +3571,73 @@ class App(QtCore.QObject):
return "ERROR: No such system parameter."
+ '''
+ Howto implement TCL shell commands:
+
+ All parameters passed to command should be possible to set as None and test it afterwards.
+ This is because we need to see error caused in tcl,
+ if None value as default parameter is not allowed TCL will return empty error.
+ Use:
+ def mycommand(name=None,...):
+
+ Test it like this:
+ if name is None:
+
+ self.raise_tcl_error('Argument name is missing.')
+
+ When error ocurre, always use raise_tcl_error, never return "sometext" on error,
+ otherwise we will miss it and processing will silently continue.
+ 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.
+
+ This behavior works only within main thread,
+ errors with promissed tasks can be catched and detected only with log.
+ TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for TCL shell.
+
+ Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
+
+ '''
+
commands = {
+ 'mytest': {
+ '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."
},
+ 'import_svg': {
+ 'fcn': import_svg,
+ 'help': "Import an SVG file as a Geometry Object.\n" +
+ "> 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' +"
- "> open_gerber [-follow <0|1>] [-outname ]\n' +"
+ 'help': "Opens a Gerber file.\n"
+ "> open_gerber [-follow <0|1>] [-outname ]\n"
" filename: Path to file to open.\n" +
" follow: If 1, does not create polygons, just follows the gerber path.\n" +
" outname: Name of the created gerber object."
@@ -2669,6 +3710,30 @@ class App(QtCore.QObject):
" gapsize: size of gap\n" +
" gaps: type of gaps"
},
+ 'geocutout': {
+ 'fcn': geocutout,
+ 'help': "Cut holding gaps from geometry.\n" +
+ "> geocutout [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps ]\n" +
+ " name: Name of the geometry object\n" +
+ " dia: Tool diameter\n" +
+ " margin: Margin over bounds\n" +
+ " gapsize: size of gap\n" +
+ " gaps: type of gaps\n" +
+ "\n" +
+ " example:\n" +
+ "\n" +
+ " #isolate margin for example from fritzing arduino shield or any svg etc\n" +
+ " isolate BCu_margin -dia 3 -overlap 1\n" +
+ "\n" +
+ " #create exteriors from isolated object\n" +
+ " exteriors BCu_margin_iso -outname BCu_margin_iso_exterior\n" +
+ "\n" +
+ " #delete isolated object if you dond need id anymore\n" +
+ " delete BCu_margin_iso\n" +
+ "\n" +
+ " #finally cut holding gaps\n" +
+ " geocutout BCu_margin_iso_exterior -dia 3 -gapsize 0.6 -gaps 4\n"
+ },
'mirror': {
'fcn': mirror,
'help': "Mirror a layer.\n" +
@@ -2678,6 +3743,33 @@ class App(QtCore.QObject):
" axis: Mirror axis parallel to the X or Y axis.\n" +
" dist: Distance of the mirror axis to the X or Y axis."
},
+ 'aligndrillgrid': {
+ 'fcn': aligndrillgrid,
+ 'help': "Create excellon with drills for aligment grid.\n" +
+ "> aligndrillgrid [-dia <3.0 (float)>] -gridx [-gridoffsetx <0 (float)>] -gridy [-gridoffsety <0 (float)>] -columns -rows \n" +
+ " outname: Name of the object to create.\n" +
+ " dia: Tool diameter\n" +
+ " gridx: grid size in X axis\n" +
+ " gridoffsetx: move grid from origin\n" +
+ " gridy: grid size in Y axis\n" +
+ " gridoffsety: move grid from origin\n" +
+ " colums: grid holes on X axis\n" +
+ " rows: grid holes on Y axis\n"
+ },
+ 'aligndrill': {
+ 'fcn': aligndrill,
+ 'help': "Create excellon with drills for aligment.\n" +
+ "> aligndrill [-dia <3.0 (float)>] -axis [-box -minoffset [-grid <10 (float)> -gridoffset <5 (float)> [-axisoffset <0 (float)>]] | -dist ]\n" +
+ " name: Name of the object (Gerber or Excellon) to mirror.\n" +
+ " dia: Tool diameter\n" +
+ " box: Name of object which act as box (cutout for example.)\n" +
+ " grid: aligning to grid, for thouse, who have aligning pins inside table in grid (-5,0),(5,0),(15,0)..." +
+ " gridoffset: offset of grid from 0 position" +
+ " minoffset: min and max distance between align hole and pcb" +
+ " axisoffset: offset on second axis before aligment holes" +
+ " axis: Mirror axis parallel to the X or Y axis.\n" +
+ " dist: Distance of the mirror axis to the X or Y axis."
+ },
'exteriors': {
'fcn': exteriors,
'help': "Get exteriors of polygons.\n" +
@@ -2708,7 +3800,7 @@ class App(QtCore.QObject):
" toolchange: Enable tool changes (example: 1)\n"
},
'millholes': {
- 'fcn': drillmillgeometry,
+ 'fcn': millholes,
'help': "Create Geometry Object for milling holes from Excellon.\n" +
"> millholes -tools -tooldia -outname \n" +
" name: Name of the Excellon Object\n" +
@@ -2737,13 +3829,15 @@ class App(QtCore.QObject):
'cncjob': {
'fcn': cncjob,
'help': 'Generates a CNC Job from a Geometry Object.\n' +
- '> cncjob [-z_cut ] [-z_move ] [-feedrate ] [-tooldia ] [-spindlespeed (int)] [-outname ]\n' +
+ '> cncjob [-z_cut ] [-z_move ] [-feedrate ] [-tooldia ] [-spindlespeed ] [-multidepth ] [-depthperpass ] [-outname ]\n' +
' name: Name of the source object\n' +
' z_cut: Z-axis cutting position\n' +
' z_move: Z-axis moving position\n' +
' feedrate: Moving speed when cutting\n' +
' tooldia: Tool diameter to show on screen\n' +
' spindlespeed: Speed of the spindle in rpm (example: 4000)\n' +
+ ' multidepth: Use or not multidepth cnccut\n'+
+ ' depthperpass: Height of one layer for multidepth\n'+
' outname: Name of the output object'
},
'write_gcode': {
@@ -2775,6 +3869,13 @@ class App(QtCore.QObject):
' name: Name of the geometry object to which to append the polygon.\n' +
' xi, yi: Coordinates of points in the polygon.'
},
+ 'subtract_poly': {
+ 'fcn': subtract_poly,
+ 'help': 'Subtract polygon from the given Geometry object.\n' +
+ '> subtract_poly [x3 y3 [...]]\n' +
+ ' name: Name of the geometry object, which will be sutracted.\n' +
+ ' xi, yi: Coordinates of points in the polygon.'
+ },
'delete': {
'fcn': delete,
'help': 'Deletes the give object.\n' +
@@ -2798,6 +3899,34 @@ class App(QtCore.QObject):
' out_name: Name of the new geometry object.' +
' obj_name_0... names of the objects to join'
},
+ 'join_excellons': {
+ 'fcn': join_excellons,
+ 'help': 'Runs a merge operation (join) on the excellon ' +
+ 'objects.' +
+ '> join_excellons ....\n' +
+ ' out_name: Name of the new excellon object.' +
+ ' obj_name_0... names of the objects to join'
+ },
+ 'panelize': {
+ 'fcn': panelize,
+ 'help': "Simple panelize geometries.\n" +
+ "> panelize [-box ] [-spacing_columns <5 (float)>] [-spacing_rows <5 (float)>] -columns -rows [-outname ]\n" +
+ " name: Name of the object to panelize.\n" +
+ " box: Name of object which act as box (cutout for example.) for cutout boundary. Object from name is used if not specified.\n" +
+ " spacing_columns: spacing between columns\n"+
+ " spacing_rows: spacing between rows\n"+
+ " columns: number of columns\n"+
+ " rows: number of rows\n"+
+ " outname: Name of the new geometry object."
+ },
+ 'subtract_rect': {
+ 'fcn': subtract_rectangle,
+ 'help': 'Subtract rectange from the given Geometry object.\n' +
+ '> subtract_rect \n' +
+ ' name: Name of the geometry object, which will be subtracted.\n' +
+ ' botleft_x, botleft_y: Coordinates of the bottom left corner.\n' +
+ ' topright_x, topright_y Coordinates of the top right corner.'
+ },
'add_rect': {
'fcn': add_rectangle,
'help': 'Creates a rectange in the given Geometry object.\n' +
@@ -2840,6 +3969,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'])
@@ -2864,14 +3996,16 @@ class App(QtCore.QObject):
"gerber": "share/flatcam_icon16.png",
"excellon": "share/drill16.png",
"cncjob": "share/cnc16.png",
- "project": "share/project16.png"
+ "project": "share/project16.png",
+ "svg": "share/geometry16.png"
}
openers = {
'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
- 'project': self.open_project
+ 'project': self.open_project,
+ 'svg': self.import_svg
}
# Open file
@@ -2905,13 +4039,17 @@ class App(QtCore.QObject):
for recent in self.recent:
filename = recent['filename'].split('/')[-1].split('\\')[-1]
- action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
+ try:
+ action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
- # Attach callback
- o = make_callback(openers[recent["kind"]], recent['filename'])
- action.triggered.connect(o)
+ # Attach callback
+ o = make_callback(openers[recent["kind"]], recent['filename'])
+ action.triggered.connect(o)
- self.ui.recent.addAction(action)
+ self.ui.recent.addAction(action)
+
+ except KeyError:
+ App.log.error("Unsupported file type: %s" % recent["kind"])
# self.builder.get_object('open_recent').set_submenu(recent_menu)
# self.ui.menufilerecent.set_submenu(recent_menu)
@@ -2930,8 +4068,6 @@ class App(QtCore.QObject):
:return: None
"""
FlatCAMObj.app = self
- ObjectCollection.app = self
- PlotCanvas.app = self
FCProcess.app = self
FCProcessContainer.app = self
@@ -3045,7 +4181,7 @@ class App(QtCore.QObject):
return
# Write
- json.dump(d, f, default=to_dict)
+ json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
# try:
# json.dump(d, f, default=to_dict)
# except Exception, e:
diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py
index ca3555b..9f22faa 100644
--- a/FlatCAMGUI.py
+++ b/FlatCAMGUI.py
@@ -23,54 +23,53 @@ class FlatCAMGUI(QtGui.QMainWindow):
# New
self.menufilenew = QtGui.QAction(QtGui.QIcon('share/file16.png'), '&New', self)
self.menufile.addAction(self.menufilenew)
+ # Open recent
+
+ # Recent
+ self.recent = self.menufile.addMenu(QtGui.QIcon('share/folder16.png'), "Open recent ...")
+
+ # Open gerber ...
+ self.menufileopengerber = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Gerber ...', self)
+ self.menufile.addAction(self.menufileopengerber)
+
+ # Open Excellon ...
+ self.menufileopenexcellon = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Excellon ...', self)
+ self.menufile.addAction(self.menufileopenexcellon)
+
+ # Open G-Code ...
+ self.menufileopengcode = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open G-&Code ...', self)
+ self.menufile.addAction(self.menufileopengcode)
# Open Project ...
self.menufileopenproject = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self)
self.menufile.addAction(self.menufileopenproject)
- # Open gerber
- self.menufileopengerber = QtGui.QAction('Open &Gerber ...', self)
- self.menufile.addAction(self.menufileopengerber)
+ # Import SVG ...
+ self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self)
+ self.menufile.addAction(self.menufileimportsvg)
- # Open Excellon ...
- self.menufileopenexcellon = QtGui.QAction('Open &Excellon ...', self)
- self.menufile.addAction(self.menufileopenexcellon)
-
- # Open G-Code ...
- self.menufileopengcode = QtGui.QAction('Open G-&Code ...', self)
- self.menufile.addAction(self.menufileopengcode)
-
- # Open recent
- # Recent
- self.recent = self.menufile.addMenu("Recent files")
-
- # Separator
- self.menufile.addSeparator()
-
- # Save Defaults
- self.menufilesavedefaults = QtGui.QAction('Save &Defaults', self)
- self.menufile.addAction(self.menufilesavedefaults)
-
- # Separator
- self.menufile.addSeparator()
+ # 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)
# Save Project As ...
- self.menufilesaveprojectas = QtGui.QAction('Save Project &As ...', self)
+ self.menufilesaveprojectas = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project &As ...', self)
self.menufile.addAction(self.menufilesaveprojectas)
# Save Project Copy ...
- self.menufilesaveprojectcopy = QtGui.QAction('Save Project C&opy ...', self)
+ self.menufilesaveprojectcopy = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project C&opy ...', self)
self.menufile.addAction(self.menufilesaveprojectcopy)
- # Separator
- self.menufile.addSeparator()
+ # Save Defaults
+ self.menufilesavedefaults = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save &Defaults', self)
+ self.menufile.addAction(self.menufilesavedefaults)
# Quit
- exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), 'E&xit', self)
+ exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), '&Exit', self)
# exitAction.setShortcut('Ctrl+Q')
# exitAction.setStatusTip('Exit application')
exit_action.triggered.connect(QtGui.qApp.quit)
@@ -145,9 +144,11 @@ class FlatCAMGUI(QtGui.QMainWindow):
### Notebook ###
################
self.notebook = QtGui.QTabWidget()
+ # self.notebook.setMinimumWidth(250)
### Projet ###
project_tab = QtGui.QWidget()
+ project_tab.setMinimumWidth(250) # Hack
self.project_tab_layout = QtGui.QVBoxLayout(project_tab)
self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
self.notebook.addTab(project_tab, "Project")
@@ -247,7 +248,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
self.setWindowIcon(self.app_icon)
self.setGeometry(100, 100, 1024, 650)
- self.setWindowTitle('FlatCAM %s' % version)
+ self.setWindowTitle('FlatCAM %s - Development Version' % version)
self.show()
def closeEvent(self, event):
@@ -421,41 +422,6 @@ class GerberOptionsGroupUI(OptionsGroupUI):
)
grid1.addWidget(self.combine_passes_cb, 3, 0)
- ## Clear non-copper regions
- self.clearcopper_label = QtGui.QLabel("Clear non-copper:")
- self.clearcopper_label.setToolTip(
- "Create a Geometry object with\n"
- "toolpaths to cut all non-copper regions."
- )
- self.layout.addWidget(self.clearcopper_label)
-
- grid5 = QtGui.QGridLayout()
- self.layout.addLayout(grid5)
- ncctdlabel = QtGui.QLabel('Tools dia:')
- ncctdlabel.setToolTip(
- "Diameters of the cutting tools, separated by ','"
- )
- grid5.addWidget(ncctdlabel, 0, 0)
- self.ncc_tool_dia_entry = FCEntry()
- grid5.addWidget(self.ncc_tool_dia_entry, 0, 1)
-
- nccoverlabel = QtGui.QLabel('Overlap:')
- nccoverlabel.setToolTip(
- "How much (fraction of tool width)\n"
- "to overlap each pass."
- )
- grid5.addWidget(nccoverlabel, 1, 0)
- self.ncc_overlap_entry = FloatEntry()
- grid5.addWidget(self.ncc_overlap_entry, 1, 1)
-
- nccmarginlabel = QtGui.QLabel('Margin:')
- nccmarginlabel.setToolTip(
- "Bounding box margin."
- )
- grid5.addWidget(nccmarginlabel, 2, 0)
- self.ncc_margin_entry = FloatEntry()
- grid5.addWidget(self.ncc_margin_entry, 2, 1)
-
## Board cuttout
self.board_cutout_label = QtGui.QLabel("Board cutout:")
self.board_cutout_label.setToolTip(
@@ -624,14 +590,39 @@ class ExcellonOptionsGroupUI(OptionsGroupUI):
self.feedrate_entry = LengthEntry()
grid1.addWidget(self.feedrate_entry, 2, 1)
+ toolchangezlabel = QtGui.QLabel('Toolchange Z:')
+ toolchangezlabel.setToolTip(
+ "Tool Z where user can change drill bit\n"
+ )
+ grid1.addWidget(toolchangezlabel, 3, 0)
+ self.toolchangez_entry = LengthEntry()
+ grid1.addWidget(self.toolchangez_entry, 3, 1)
+
spdlabel = QtGui.QLabel('Spindle speed:')
spdlabel.setToolTip(
"Speed of the spindle\n"
"in RPM (optional)"
)
- grid1.addWidget(spdlabel, 3, 0)
+ grid1.addWidget(spdlabel, 4, 0)
self.spindlespeed_entry = IntEntry(allow_empty=True)
- grid1.addWidget(self.spindlespeed_entry, 3, 1)
+ grid1.addWidget(self.spindlespeed_entry, 4, 1)
+
+ #### Milling Holes ####
+ self.mill_hole_label = QtGui.QLabel('Mill Holes')
+ self.mill_hole_label.setToolTip(
+ "Create Geometry for milling holes."
+ )
+ self.layout.addWidget(self.mill_hole_label)
+
+ grid1 = QtGui.QGridLayout()
+ self.layout.addLayout(grid1)
+ tdlabel = QtGui.QLabel('Tool dia:')
+ tdlabel.setToolTip(
+ "Diameter of the cutting tool."
+ )
+ grid1.addWidget(tdlabel, 0, 0)
+ self.tooldia_entry = LengthEntry()
+ grid1.addWidget(self.tooldia_entry, 0, 1)
class GeometryOptionsGroupUI(OptionsGroupUI):
@@ -815,6 +806,26 @@ class CNCJobOptionsGroupUI(OptionsGroupUI):
self.append_text = FCTextArea()
self.layout.addWidget(self.append_text)
+ # Dwell
+ grid1 = QtGui.QGridLayout()
+ self.layout.addLayout(grid1)
+
+ dwelllabel = QtGui.QLabel('Dwell:')
+ dwelllabel.setToolTip(
+ "Pause to allow the spindle to reach its\n"
+ "speed before cutting."
+ )
+ dwelltime = QtGui.QLabel('Duration [sec.]:')
+ dwelltime.setToolTip(
+ "Number of second to dwell."
+ )
+ self.dwell_cb = FCCheckBox()
+ self.dwelltime_cb = FCEntry()
+ grid1.addWidget(dwelllabel, 0, 0)
+ grid1.addWidget(self.dwell_cb, 0, 1)
+ grid1.addWidget(dwelltime, 1, 0)
+ grid1.addWidget(self.dwelltime_cb, 1, 1)
+
class GlobalOptionsUI(QtGui.QWidget):
"""
@@ -870,4 +881,4 @@ class GlobalOptionsUI(QtGui.QWidget):
#
#
# if __name__ == '__main__':
-# main()
+# main()
\ No newline at end of file
diff --git a/FlatCAMObj.py b/FlatCAMObj.py
index 5855a3b..8860bcb 100644
--- a/FlatCAMObj.py
+++ b/FlatCAMObj.py
@@ -1,3 +1,4 @@
+from cStringIO import StringIO
from PyQt4 import QtCore
from copy import copy
from ObjectUI import *
@@ -6,9 +7,7 @@ import inspect # TODO: For debugging only.
from camlib import *
from FlatCAMCommon import LoudDict
from FlatCAMDraw import FlatCAMDraw
-from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
-TOLERANCE = 0.01
########################################
## FlatCAMObj ##
@@ -43,8 +42,6 @@ class FlatCAMObj(QtCore.QObject):
self.axes = None # Matplotlib axes
self.kind = None # Override with proper name
- self.item = None # Link with project view item
-
self.muted_ui = False
# assert isinstance(self.ui, ObjectUI)
@@ -52,6 +49,23 @@ class FlatCAMObj(QtCore.QObject):
# self.ui.offset_button.clicked.connect(self.on_offset_button_click)
# self.ui.scale_button.clicked.connect(self.on_scale_button_click)
+ def from_dict(self, d):
+ """
+ This supersedes ``from_dict`` in derived classes. Derived classes
+ must inherit from FlatCAMObj first, then from derivatives of Geometry.
+
+ ``self.options`` is only updated, not overwritten. This ensures that
+ options set by the app do not vanish when reading the objects
+ from a project file.
+ """
+
+ for attr in self.ser_attrs:
+
+ if attr == 'options':
+ self.options.update(d[attr])
+ else:
+ setattr(self, attr, d[attr])
+
def on_options_change(self, key):
self.emit(QtCore.SIGNAL("optionChanged"), key)
@@ -61,19 +75,18 @@ class FlatCAMObj(QtCore.QObject):
self.form_fields = {"name": self.ui.name_entry}
assert isinstance(self.ui, ObjectUI)
- self.ui.name_entry.editingFinished.connect(self.on_name_editing_finished)
+ self.ui.name_entry.returnPressed.connect(self.on_name_activate)
self.ui.offset_button.clicked.connect(self.on_offset_button_click)
self.ui.scale_button.clicked.connect(self.on_scale_button_click)
def __str__(self):
return "".format(self.kind, self.options["name"])
- def on_name_editing_finished(self):
+ def on_name_activate(self):
old_name = copy(self.options["name"])
new_name = self.ui.name_entry.get_value()
- if new_name != old_name:
- self.options["name"] = new_name
- self.app.info("Name changed from %s to %s" % (old_name, new_name))
+ self.options["name"] = self.ui.name_entry.get_value()
+ self.app.info("Name changed from %s to %s" % (old_name, new_name))
def on_offset_button_click(self):
self.app.report_usage("obj_on_offset_button")
@@ -90,14 +103,50 @@ class FlatCAMObj(QtCore.QObject):
self.scale(factor)
self.plot()
+ def setup_axes(self, figure):
+ """
+ 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
+ them to figure if not part of the figure. 4) Sets transparent
+ background. 5) Sets 1:1 scale aspect ratio.
+
+ :param figure: A Matplotlib.Figure on which to add/configure axes.
+ :type figure: matplotlib.figure.Figure
+ :return: None
+ :rtype: None
+ """
+
+ if self.axes is None:
+ FlatCAMApp.App.log.debug("setup_axes(): New axes")
+ self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
+ label=self.options["name"])
+ elif self.axes not in figure.axes:
+ FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
+ self.axes.cla()
+ figure.add_axes(self.axes)
+ else:
+ FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes")
+ self.axes.cla()
+
+ # Remove all decoration. The app's axes will have
+ # the ticks and grid.
+ self.axes.set_frame_on(False) # No frame
+ self.axes.set_xticks([]) # No tick
+ self.axes.set_yticks([]) # No ticks
+ self.axes.patch.set_visible(False) # No background
+ self.axes.set_aspect(1)
+
def to_form(self):
"""
Copies options to the UI form.
:return: None
"""
+ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.to_form()")
for option in self.options:
- self.set_form_item(option)
+ try:
+ self.set_form_item(option)
+ except:
+ self.app.log.warning("Unexpected error:", sys.exc_info())
def read_form(self):
"""
@@ -108,7 +157,10 @@ class FlatCAMObj(QtCore.QObject):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
for option in self.options:
- self.read_form_item(option)
+ try:
+ self.read_form_item(option)
+ except:
+ self.app.log.warning("Unexpected error:", sys.exc_info())
def build_ui(self):
"""
@@ -137,8 +189,8 @@ class FlatCAMObj(QtCore.QObject):
self.app.ui.selected_scroll_area.takeWidget()
except:
self.app.log.debug("Nothing to remove")
-
self.app.ui.selected_scroll_area.setWidget(self.ui)
+ self.to_form()
self.muted_ui = False
@@ -170,6 +222,17 @@ class FlatCAMObj(QtCore.QObject):
except KeyError:
self.app.log.warning("Failed to read option from field: %s" % option)
+ # #try read field only when option have equivalent in form_fields
+ # if option in self.form_fields:
+ # option_type=type(self.options[option])
+ # try:
+ # value=self.form_fields[option].get_value()
+ # #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either.
+ # except (KeyError,SyntaxError):
+ # self.app.log.warning("Failed to read option from field: %s" % option)
+ # else:
+ # self.app.log.warning("Form fied does not exists: %s" % option)
+
def plot(self):
"""
Plot this object (Extend this method to implement the actual plotting).
@@ -192,7 +255,6 @@ class FlatCAMObj(QtCore.QObject):
# Clear axes or we will plot on top of them.
self.axes.cla() # TODO: Thread safe?
-
return True
def serialize(self):
@@ -238,9 +300,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": 0.016,
"isopasses": 1,
"isooverlap": 0.15,
- "ncctools": "1.0, 0.5",
- "nccoverlap": 0.4,
- "nccmargin": 1,
"combine_passes": True,
"cutouttooldia": 0.07,
"cutoutmargin": 0.2,
@@ -286,9 +345,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": self.ui.iso_tool_dia_entry,
"isopasses": self.ui.iso_width_entry,
"isooverlap": self.ui.iso_overlap_entry,
- "ncctools": self.ui.ncc_tool_dia_entry,
- "nccoverlap": self.ui.ncc_overlap_entry,
- "nccmargin": self.ui.ncc_margin_entry,
"combine_passes": self.ui.combine_passes_cb,
"cutouttooldia": self.ui.cutout_tooldia_entry,
"cutoutmargin": self.ui.cutout_margin_entry,
@@ -300,15 +356,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"bboxrounded": self.ui.bbrounded_cb
})
- # Fill form fields only on object create
- self.to_form()
-
assert isinstance(self.ui, GerberObjectUI)
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
- self.ui.generate_ncc_button.clicked.connect(self.on_ncc_button_click)
self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
@@ -351,7 +403,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
name = self.options["name"] + "_cutout"
def geo_init(geo_obj, app_obj):
- margin = self.options["cutoutmargin"] + self.options["cutouttooldia"] / 2
+ margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
minx, miny, maxx, maxy = self.bounds()
minx -= margin
@@ -392,89 +444,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.read_form()
self.isolate()
- def on_ncc_button_click(self, *args):
- self.app.report_usage("gerber_on_ncc_button")
-
- # Prepare parameters
- try:
- tools = [float(eval(dia)) for dia in self.ui.ncc_tool_dia_entry.get_value().split(",")]
- except:
- FlatCAMApp.App.log.error("At least one tool diameter needed")
- return
-
- over = self.ui.ncc_overlap_entry.get_value()
- margin = self.ui.ncc_margin_entry.get_value()
-
- if over is None or margin is None:
- FlatCAMApp.App.log.error("Overlap and margin values needed")
- return
-
- print "non-copper clear button clicked", tools, over, margin
-
- # Sort tools in descending order
- tools.sort(reverse=True)
-
- # Prepare non-copper polygons
- bounding_box = self.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
- empty = self.get_empty_area(bounding_box)
- if type(empty) is Polygon:
- empty = MultiPolygon([empty])
-
- # Main procedure
- def clear_non_copper():
-
- # Already cleared area
- cleared = MultiPolygon()
-
- # Geometry object creating callback
- def geo_init(geo_obj, app_obj):
- geo_obj.options["cnctooldia"] = tool
- geo_obj.solid_geometry = []
- for p in area.geoms:
- try:
- cp = self.clear_polygon(p, tool, over)
- geo_obj.solid_geometry.append(list(cp.get_objects()))
- except:
- FlatCAMApp.App.log.warning("Polygon is ommited")
-
- # Generate area for each tool
- offset = sum(tools)
- for tool in tools:
- # Get remaining tools offset
- offset -= tool
-
- # Area to clear
- area = empty.buffer(-offset).difference(cleared)
-
- # Transform area to MultiPolygon
- if type(area) is Polygon:
- area = MultiPolygon([area])
-
- # Check if area not empty
- if len(area.geoms) > 0:
- # Overall cleared area
- cleared = empty.buffer(-offset * (1 + over)).buffer(-tool / 2).buffer(tool / 2)
-
- # Create geometry object
- name = self.options["name"] + "_ncc_" + repr(tool) + "D"
- self.app.new_object("geometry", name, geo_init)
- else:
- return
-
- # Do job in background
- proc = self.app.proc_container.new("Clearing non-copper areas.")
-
- def job_thread(app_obj):
- try:
- clear_non_copper()
- except Exception as e:
- proc.done()
- raise e
- proc.done()
-
- self.app.inform.emit("Clear non-copper areas started ...")
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
-
def follow(self, outname=None):
"""
Creates a geometry object "following" the gerber paths.
@@ -548,7 +517,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
geo_obj.solid_geometry = []
for i in range(passes):
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
- geom = generate_envelope(offset, i == 0)
+ geom = generate_envelope (offset, i == 0)
geo_obj.solid_geometry.append(geom)
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
@@ -558,7 +527,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else:
for i in range(passes):
- offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
+ offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
if passes > 1:
iso_name = base_name + str(i + 1)
else:
@@ -568,7 +537,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = self.options["isotooldia"]
- geo_obj.solid_geometry = generate_envelope(offset, i == 0)
+ geo_obj.solid_geometry = generate_envelope (offset, i == 0)
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
# TODO: Do something if this is None. Offer changing name?
@@ -637,7 +606,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
for poly in geometry:
# TODO: Too many things hardcoded.
try:
- poly = poly.simplify(TOLERANCE)
patch = PolygonPatch(poly,
facecolor="#BBF268",
edgecolor="#006E20",
@@ -649,7 +617,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
FlatCAMApp.App.log.warning(str(poly))
else:
for poly in geometry:
- poly = poly.simplify(TOLERANCE)
x, y = poly.exterior.xy
self.axes.plot(x, y, linespec)
for ints in poly.interiors:
@@ -699,6 +666,76 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# from predecessors.
self.ser_attrs += ['options', 'kind']
+ @staticmethod
+ def merge(exc_list, exc_final):
+ """
+ Merge excellons in exc_list into exc_final.
+ Options are allways copied from source .
+
+ Tools are also merged, if name for tool is same and size differs, then as name is used next available number from both lists
+
+ if only one object is specified in exc_list then this acts as copy only
+
+ :param exc_list: List or one object of FlatCAMExcellon Objects to join.
+ :param exc_final: Destination FlatCAMExcellon object.
+ :return: None
+ """
+
+ if type(exc_list) is not list:
+ exc_list_real= list()
+ exc_list_real.append(exc_list)
+ else:
+ exc_list_real=exc_list
+
+ for exc in exc_list_real:
+ # Expand lists
+ if type(exc) is list:
+ FlatCAMExcellon.merge(exc, exc_final)
+ # If not list, merge excellons
+ else:
+
+ # TODO: I realize forms does not save values into options , when object is deselected
+ # leave this here for future use
+ # this reinitialize options based on forms, all steps may not be necessary
+ # exc.app.collection.set_active(exc.options['name'])
+ # exc.to_form()
+ # exc.read_form()
+ for option in exc.options:
+ if option is not 'name':
+ try:
+ exc_final.options[option] = exc.options[option]
+ except:
+ exc.app.log.warning("Failed to copy option.",option)
+
+ #deep copy of all drills,to avoid any references
+ for drill in exc.drills:
+ point = Point(drill['point'].x,drill['point'].y)
+ exc_final.drills.append({"point": point, "tool": drill['tool']})
+ toolsrework=dict()
+ max_numeric_tool=0
+ for toolname in exc.tools.iterkeys():
+ numeric_tool=int(toolname)
+ if numeric_tool>max_numeric_tool:
+ max_numeric_tool=numeric_tool
+ toolsrework[exc.tools[toolname]['C']]=toolname
+
+ #exc_final as last because names from final tools will be used
+ for toolname in exc_final.tools.iterkeys():
+ numeric_tool=int(toolname)
+ if numeric_tool>max_numeric_tool:
+ max_numeric_tool=numeric_tool
+ toolsrework[exc_final.tools[toolname]['C']]=toolname
+
+ for toolvalues in toolsrework.iterkeys():
+ if toolsrework[toolvalues] in exc_final.tools:
+ if exc_final.tools[toolsrework[toolvalues]]!={"C": toolvalues}:
+ exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues}
+ else:
+ exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues}
+ #this value was not co
+ exc_final.zeros=exc.zeros
+ exc_final.create_geometry()
+
def build_ui(self):
FlatCAMObj.build_ui(self)
@@ -717,6 +754,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
dia.setFlags(QtCore.Qt.ItemIsEnabled)
self.ui.tools_table.setItem(i, 1, dia) # Diameter
i += 1
+
+ # sort the tool diameter column
+ self.ui.tools_table.sortItems(1)
+ # all the tools are selected by default
+ self.ui.tools_table.selectColumn(0)
+
self.ui.tools_table.resizeColumnsToContents()
self.ui.tools_table.resizeRowsToContents()
self.ui.tools_table.horizontalHeader().setStretchLastSection(True)
@@ -748,9 +791,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
"spindlespeed": self.ui.spindlespeed_entry
})
- # Fill form fields
- self.to_form()
-
assert isinstance(self.ui, ExcellonObjectUI), \
"Expected a ExcellonObjectUI, got %s" % type(self.ui)
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
@@ -959,7 +999,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"plot": True,
"tooldia": 0.4 / 25.4, # 0.4mm in inches
"append": "",
- "prepend": ""
+ "prepend": "",
+ "dwell": False,
+ "dwelltime": 1
})
# Attributes to be included in serialization
@@ -979,12 +1021,11 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"plot": self.ui.plot_cb,
"tooldia": self.ui.tooldia_entry,
"append": self.ui.append_text,
- "prepend": self.ui.prepend_text
+ "prepend": self.ui.prepend_text,
+ "dwell": self.ui.dwell_cb,
+ "dwelltime": self.ui.dwelltime_entry
})
- # Fill form fields only on object create
- self.to_form()
-
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
@@ -1000,6 +1041,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
def on_exportgcode_button_click(self, *args):
self.app.report_usage("cncjob_on_exportgcode_button")
+ self.read_form()
+
try:
filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...",
directory=self.app.defaults["last_folder"])
@@ -1011,16 +1054,69 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
self.export_gcode(filename, preamble=preamble, postamble=postamble)
+ def dwell_generator(self, lines):
+ """
+ Inserts "G4 P..." instructions after spindle-start
+ instructions (M03 or M04).
+
+ """
+
+ log.debug("dwell_generator()...")
+
+ m3m4re = re.compile(r'^\s*[mM]0[34]')
+ g4re = re.compile(r'^\s*[gG]4\s+([\d\.\+\-e]+)')
+ bufline = None
+
+ for line in lines:
+ # If the buffer contains a G4, yield that.
+ # If current line is a G4, discard it.
+ if bufline is not None:
+ yield bufline
+ bufline = None
+
+ if not g4re.search(line):
+ yield line
+
+ continue
+
+ # If start spindle, buffer a G4.
+ if m3m4re.search(line):
+ log.debug("Found M03/4")
+ bufline = "G4 P{}\n".format(self.options['dwelltime'])
+
+ yield line
+
+ raise StopIteration
+
def export_gcode(self, filename, preamble='', postamble=''):
- f = open(filename, 'w')
- f.write(preamble + '\n' + self.gcode + "\n" + postamble)
- f.close()
+
+ lines = StringIO(self.gcode)
+
+ ## Post processing
+ # Dwell?
+ if self.options['dwell']:
+ log.debug("Will add G04!")
+ lines = self.dwell_generator(lines)
+
+ ## Write
+ with open(filename, 'w') as f:
+ f.write(preamble + "\n")
+
+ for line in lines:
+
+ f.write(line)
+
+ f.write(postamble)
# Just for adding it to the recent files list.
self.app.file_opened.emit("cncjob", filename)
self.app.inform.emit("Saved to: " + filename)
+ def get_gcode(self, preamble='', postamble=''):
+ #we need this to beable get_gcode separatelly for shell command export_code
+ return preamble + '\n' + self.gcode + "\n" + postamble
+
def on_plot_cb_click(self, *args):
if self.muted_ui:
return
@@ -1034,7 +1130,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
if not FlatCAMObj.plot(self):
return
- self.plot2(self.axes, tooldia=self.options["tooldia"], tool_tolerance=TOLERANCE)
+ self.plot2(self.axes, tooldia=self.options["tooldia"])
self.app.plotcanvas.auto_adjust_axes()
@@ -1078,12 +1174,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
else:
geo_final.solid_geometry.append(geo.solid_geometry)
- # try: # Iterable
- # for shape in geo.solid_geometry:
- # geo_final.solid_geometry.append(shape)
- #
- # except TypeError: # Non-iterable
- # geo_final.solid_geometry.append(geo.solid_geometry)
+ # try: # Iterable
+ # for shape in geo.solid_geometry:
+ # geo_final.solid_geometry.append(shape)
+ #
+ # except TypeError: # Non-iterable
+ # geo_final.solid_geometry.append(geo.solid_geometry)
def __init__(self, name):
FlatCAMObj.__init__(self, name)
@@ -1137,15 +1233,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"depthperpass": self.ui.maxdepth_entry
})
- # Fill form fields only on object create
- self.to_form()
-
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
def on_paint_button_click(self, *args):
-
self.app.report_usage("geometry_on_paint_button")
self.app.info("Click inside the desired polygon.")
@@ -1168,7 +1260,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
def paint_poly(self, inside_pt, tooldia, overlap):
# Which polygon.
- # poly = find_polygon(self.solid_geometry, inside_pt)
+ #poly = find_polygon(self.solid_geometry, inside_pt)
poly = self.find_polygon(inside_pt)
# No polygon?
@@ -1185,7 +1277,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
def gen_paintarea(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry), \
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
- # assert isinstance(app_obj, App)
+ #assert isinstance(app_obj, App)
if self.options["paintmethod"] == "seed":
cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]),
@@ -1228,7 +1320,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
@@ -1289,18 +1382,21 @@ 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)
+ 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)
- # Create a promise with the name
- self.app.collection.promise(outname)
+ # 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]})
+ # 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:
@@ -1337,11 +1433,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
dx, dy = vect
- if type(self.solid_geometry) == list:
- self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
- for g in self.solid_geometry]
- else:
- self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
+ def translate_recursion(geom):
+ if type(geom) == list:
+ geoms=list()
+ for local_geom in geom:
+ geoms.append(translate_recursion(local_geom))
+ return geoms
+ else:
+ return affinity.translate(geom, xoff=dx, yoff=dy)
+
+ self.solid_geometry=translate_recursion(self.solid_geometry)
def convert_units(self, units):
factor = Geometry.convert_units(self, units)
@@ -1363,7 +1464,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
except TypeError: # Element is not iterable...
if type(element) == Polygon:
- x, y = element.simplify(TOLERANCE).exterior.coords.xy
+ x, y = element.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in element.interiors:
x, y = ints.coords.xy
@@ -1371,7 +1472,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
return
if type(element) == LineString or type(element) == LinearRing:
- x, y = element.simplify(TOLERANCE).coords.xy
+ x, y = element.coords.xy
self.axes.plot(x, y, 'r-')
return
diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py
index 528171b..8c13f4b 100644
--- a/FlatCAMWorker.py
+++ b/FlatCAMWorker.py
@@ -1,5 +1,4 @@
from PyQt4 import QtCore
-#import FlatCAMApp
class Worker(QtCore.QObject):
@@ -8,31 +7,52 @@ class Worker(QtCore.QObject):
in a single independent thread.
"""
+ # avoid multiple tests for debug availability
+ pydevd_failed = False
+
def __init__(self, app, name=None):
super(Worker, self).__init__()
self.app = app
self.name = name
+ def allow_debug(self):
+ """
+ allow debuging/breakpoints in this threads
+ should work from PyCharm and PyDev
+ :return:
+ """
+
+ if not self.pydevd_failed:
+ try:
+ import pydevd
+ pydevd.settrace(suspend=False, trace_only_current_thread=True)
+ except ImportError:
+ self.pydevd_failed=True
+
def run(self):
- # FlatCAMApp.App.log.debug("Worker Started!")
+
self.app.log.debug("Worker Started!")
+ self.allow_debug()
+
# Tasks are queued in the event listener.
self.app.worker_task.connect(self.do_worker_task)
def do_worker_task(self, task):
- # FlatCAMApp.App.log.debug("Running task: %s" % str(task))
+
self.app.log.debug("Running task: %s" % str(task))
- # 'worker_name' property of task allows to target
- # specific worker.
- if 'worker_name' in task and task['worker_name'] == self.name:
- task['fcn'](*task['params'])
+ self.allow_debug()
+
+ if ('worker_name' in task and task['worker_name'] == self.name) or \
+ ('worker_name' not in task and self.name is None):
+
+ try:
+ task['fcn'](*task['params'])
+ except Exception as e:
+ self.app.thread_exception.emit(e)
+ raise e
+
return
- if 'worker_name' not in task and self.name is None:
- task['fcn'](*task['params'])
- 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/GUIElements.py b/GUIElements.py
index 87b9e21..929b04a 100644
--- a/GUIElements.py
+++ b/GUIElements.py
@@ -221,6 +221,9 @@ class FCCheckBox(QtGui.QCheckBox):
def set_value(self, val):
self.setChecked(val)
+ def toggle(self):
+ self.set_value(not self.get_value())
+
class FCTextArea(QtGui.QPlainTextEdit):
def __init__(self, parent=None):
diff --git a/ObjectCollection.py b/ObjectCollection.py
index eba40f6..e423e5d 100644
--- a/ObjectCollection.py
+++ b/ObjectCollection.py
@@ -5,18 +5,11 @@ import FlatCAMApp
from PyQt4 import Qt, QtGui, QtCore
-class KeySensitiveListView(QtGui.QTreeView):
+class KeySensitiveListView(QtGui.QListView):
"""
QtGui.QListView extended to emit a signal on key press.
"""
- def __init__(self, parent = None):
- super(KeySensitiveListView, self).__init__(parent)
- self.setHeaderHidden(True)
- self.setEditTriggers(QtGui.QTreeView.SelectedClicked)
- # self.setRootIsDecorated(False)
- # self.setExpandsOnDoubleClick(False)
-
keyPressed = QtCore.pyqtSignal(int)
def keyPressEvent(self, event):
@@ -24,70 +17,11 @@ class KeySensitiveListView(QtGui.QTreeView):
self.keyPressed.emit(event.key())
-class TreeItem:
- """
- Item of a tree model
- """
- def __init__(self, data, icon = None, obj = None, parent_item = None):
-
- self.parent_item = parent_item
- self.item_data = data # Columns string data
- self.icon = icon # Decoration
- self.obj = obj # FlatCAMObj
-
- self.child_items = []
-
- if parent_item:
- parent_item.append_child(self)
-
- def append_child(self, item):
- self.child_items.append(item)
- item.set_parent_item(self)
-
- def remove_child(self, item):
- child = self.child_items.pop(self.child_items.index(item))
- del child
-
- def remove_children(self):
- for child in self.child_items:
- del child
-
- self.child_items = []
-
- def child(self, row):
- return self.child_items[row]
-
- def child_count(self):
- return len(self.child_items)
-
- def column_count(self):
- return len(self.item_data)
-
- def data(self, column):
- return self.item_data[column]
-
- def row(self):
- return self.parent_item.child_items.index(self)
-
- def set_parent_item(self, parent_item):
- self.parent_item = parent_item
-
- def __del__(self):
- del self.obj
- del self.icon
-
-class ObjectCollection(QtCore.QAbstractItemModel):
+class ObjectCollection(QtCore.QAbstractListModel):
"""
Object storage and management.
"""
- groups = [
- ("gerber", "Gerber"),
- ("excellon", "Excellon"),
- ("geometry", "Geometry"),
- ("cncjob", "CNC Job")
- ]
-
classdict = {
"gerber": FlatCAMGerber,
"excellon": FlatCAMExcellon,
@@ -102,34 +36,15 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"geometry": "share/geometry16.png"
}
- root_item = None
- app = None
-
def __init__(self, parent=None):
-
- QtCore.QAbstractItemModel.__init__(self, parent=parent)
-
+ QtCore.QAbstractListModel.__init__(self, parent=parent)
### Icons for the list view
self.icons = {}
for kind in ObjectCollection.icon_files:
self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
- # Create root tree view item
- self.root_item = TreeItem(["root"])
-
- # Create group items
- self.group_items = {}
- for kind, title in ObjectCollection.groups:
- item = TreeItem([title], self.icons[kind])
- self.group_items[kind] = item
- self.root_item.append_child(item)
-
- # Create test sub-items
- # for i in self.root_item.m_child_items:
- # print i.data(0)
- # i.append_child(TreeItem(["empty"]))
-
### Data ###
+ self.object_list = []
self.checked_indexes = []
# Names of objects that are expected to become available.
@@ -140,6 +55,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
self.promises = set()
### View
+ #self.view = QtGui.QListView()
self.view = KeySensitiveListView()
self.view.setSelectionMode(Qt.QAbstractItemView.ExtendedSelection)
self.view.setModel(self)
@@ -162,114 +78,41 @@ class ObjectCollection(QtCore.QAbstractItemModel):
def on_key(self, key):
# Delete
- active = self.get_active()
- if key == QtCore.Qt.Key_Delete and active:
+ if key == QtCore.Qt.Key_Delete:
# Delete via the application to
# ensure cleanup of the GUI
- active.app.on_delete()
+ self.get_active().app.on_delete()
+ return
+
+ if key == QtCore.Qt.Key_Space:
+ self.get_active().ui.plot_cb.toggle()
+ return
def on_mouse_down(self, event):
FlatCAMApp.App.log.debug("Mouse button pressed on list")
- def index(self, row, column = 0, parent = None, *args, **kwargs):
- if not self.hasIndex(row, column, parent):
- return QtCore.QModelIndex()
+ def rowCount(self, parent=QtCore.QModelIndex(), *args, **kwargs):
+ return len(self.object_list)
- if not parent.isValid():
- parent_item = self.root_item
- else:
- parent_item = parent.internalPointer()
+ def columnCount(self, *args, **kwargs):
+ return 1
- child_item = parent_item.child(row)
-
- if child_item:
- return self.createIndex(row, column, child_item)
- else:
- return QtCore.QModelIndex()
-
- def parent(self, index = None):
- if not index.isValid():
- return QtCore.QModelIndex()
-
- parent_item = index.internalPointer().parent_item
-
- if parent_item == self.root_item:
- return QtCore.QModelIndex()
-
- return self.createIndex(parent_item.row(), 0, parent_item)
-
- def rowCount(self, index = None, *args, **kwargs):
- if index.column() > 0:
- return 0
-
- if not index.isValid():
- parent_item = self.root_item
- else:
- parent_item = index.internalPointer()
-
- return parent_item.child_count()
-
- def columnCount(self, index = None, *args, **kwargs):
- if index.isValid():
- return index.internalPointer().column_count()
- else:
- return self.root_item.column_count()
-
- def data(self, index, role = None):
- if not index.isValid():
+ def data(self, index, role=Qt.Qt.DisplayRole):
+ if not index.isValid() or not 0 <= index.row() < self.rowCount():
return QtCore.QVariant()
-
- if role in [Qt.Qt.DisplayRole, Qt.Qt.EditRole]:
- obj = index.internalPointer().obj
- if obj:
- return obj.options["name"]
- else:
- return index.internalPointer().data(index.column())
-
- elif role == Qt.Qt.DecorationRole:
- icon = index.internalPointer().icon
- if icon:
- return icon
- else:
- return Qt.QPixmap()
- else:
- return QtCore.QVariant()
-
- def setData(self, index, data, role = None):
- if index.isValid():
- obj = index.internalPointer().obj
- if obj:
- obj.options["name"] = data.toString()
- obj.build_ui()
-
- def flags(self, index):
- if not index.isValid():
- return 0
-
- # Prevent groups from selection
- if not index.internalPointer().obj:
- return Qt.Qt.ItemIsEnabled
- else:
- return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable | Qt.Qt.ItemIsEditable
-
- return QtCore.QAbstractItemModel.flags(self, index)
-
- # def data(self, index, role=Qt.Qt.DisplayRole):
- # if not index.isValid() or not 0 <= index.row() < self.rowCount():
- # return QtCore.QVariant()
- # row = index.row()
- # if role == Qt.Qt.DisplayRole:
- # return self.object_list[row].options["name"]
- # if role == Qt.Qt.DecorationRole:
- # return self.icons[self.object_list[row].kind]
- # # if role == Qt.Qt.CheckStateRole:
- # # if row in self.checked_indexes:
- # # return Qt.Qt.Checked
- # # else:
- # # return Qt.Qt.Unchecked
+ row = index.row()
+ if role == Qt.Qt.DisplayRole:
+ return self.object_list[row].options["name"]
+ if role == Qt.Qt.DecorationRole:
+ return self.icons[self.object_list[row].kind]
+ # if role == Qt.Qt.CheckStateRole:
+ # if row in self.checked_indexes:
+ # return Qt.Qt.Checked
+ # else:
+ # return Qt.Qt.Unchecked
def print_list(self):
- for obj in self.get_list():
+ for obj in self.object_list:
print obj
def append(self, obj, active=False):
@@ -300,20 +143,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
obj.set_ui(obj.ui_type())
# Required before appending (Qt MVC)
- group = self.group_items[obj.kind]
- group_index = self.index(group.row(), 0, Qt.QModelIndex())
- self.beginInsertRows(group_index, group.child_count(), group.child_count())
+ self.beginInsertRows(QtCore.QModelIndex(), len(self.object_list), len(self.object_list))
- # Append new item
- obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
+ # Simply append to the python list
+ self.object_list.append(obj)
# Required after appending (Qt MVC)
self.endInsertRows()
- # Expand group
- if group.child_count() is 1:
- self.view.setExpanded(group_index, True)
-
def get_names(self):
"""
Gets a list of the names of all objects in the collection.
@@ -323,7 +160,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
- return [x.options['name'] for x in self.get_list()]
+ return [x.options['name'] for x in self.object_list]
def get_bounds(self):
"""
@@ -341,8 +178,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
xmax = -Inf
ymax = -Inf
- # for obj in self.object_list:
- for obj in self.get_list():
+ for obj in self.object_list:
try:
gxmin, gymin, gxmax, gymax = obj.bounds()
xmin = min([xmin, gxmin])
@@ -365,7 +201,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
- for obj in self.get_list():
+ for obj in self.object_list:
if obj.options['name'] == name:
return obj
return None
@@ -374,12 +210,12 @@ class ObjectCollection(QtCore.QAbstractItemModel):
selections = self.view.selectedIndexes()
if len(selections) == 0:
return
+ row = selections[0].row()
- active = selections[0].internalPointer()
- group = active.parent_item
+ self.beginRemoveRows(QtCore.QModelIndex(), row, row)
+
+ self.object_list.pop(row)
- self.beginRemoveRows(self.index(group.row(), 0, Qt.QModelIndex()), active.row(), active.row())
- group.remove_child(active)
self.endRemoveRows()
def get_active(self):
@@ -391,8 +227,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
selections = self.view.selectedIndexes()
if len(selections) == 0:
return None
-
- return selections[0].internalPointer().obj
+ row = selections[0].row()
+ return self.object_list[row]
def get_selected(self):
"""
@@ -400,7 +236,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:return: List of objects
"""
- return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
+ return [self.object_list[sel.row()] for sel in self.view.selectedIndexes()]
def set_active(self, name):
"""
@@ -410,34 +246,40 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:param name: Name of the FlatCAM Object
:return: None
"""
- obj = self.get_by_name(name)
- item = obj.item
- group = self.group_items[obj.kind]
+ iobj = self.createIndex(self.get_names().index(name), 0) # Column 0
+ self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select)
- group_index = self.index(group.row(), 0, Qt.QModelIndex())
- item_index = self.index(item.row(), 0, group_index)
+ def set_inactive(self, name):
+ """
+ Unselect object by name from the project list. This triggers the
+ list_selection_changed event and call on_list_selection_changed.
- self.view.selectionModel().select(item_index, QtGui.QItemSelectionModel.Select)
+ :param name: Name of the FlatCAM Object
+ :return: None
+ """
+ iobj = self.createIndex(self.get_names().index(name), 0) # Column 0
+ self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Deselect)
+
+ def set_all_inactive(self):
+ """
+ Unselect all objects from the project list. This triggers the
+ list_selection_changed event and call on_list_selection_changed.
+
+ :return: None
+ """
+ for name in self.get_names():
+ self.set_inactive(name)
def on_list_selection_change(self, current, previous):
FlatCAMApp.App.log.debug("on_list_selection_change()")
FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
-
try:
- obj = current.indexes()[0].internalPointer().obj
+ selection_index = current.indexes()[0].row()
except IndexError:
FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
-
- try:
- self.app.ui.selected_scroll_area.takeWidget()
- except:
- FlatCAMApp.App.log.debug("Nothing to remove")
-
- self.app.setup_component_editor()
return
- if obj:
- obj.build_ui()
+ self.object_list[selection_index].build_ui()
def on_item_activated(self, index):
"""
@@ -446,23 +288,18 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:param index: Index of the item in the list.
:return: None
"""
- index.internalPointer().obj.build_ui()
+ self.object_list[index.row()].build_ui()
def delete_all(self):
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
self.beginResetModel()
- self.checked_indexes = []
- for group in self.root_item.child_items:
- group.remove_children()
+ self.object_list = []
+ self.checked_indexes = []
self.endResetModel()
def get_list(self):
- obj_list = []
- for group in self.root_item.child_items:
- for item in group.child_items:
- obj_list.append(item.obj)
+ return self.object_list
- return obj_list
diff --git a/ObjectUI.py b/ObjectUI.py
index 92022dd..532066f 100644
--- a/ObjectUI.py
+++ b/ObjectUI.py
@@ -163,7 +163,9 @@ class CNCObjectUI(ObjectUI):
)
self.custom_box.addWidget(self.updateplot_button)
+ ##################
## Export G-Code
+ ##################
self.export_gcode_label = QtGui.QLabel("Export G-Code:")
self.export_gcode_label.setToolTip(
"Export and save G-Code to\n"
@@ -194,6 +196,26 @@ class CNCObjectUI(ObjectUI):
self.append_text = FCTextArea()
self.custom_box.addWidget(self.append_text)
+ # Dwell
+ grid1 = QtGui.QGridLayout()
+ self.custom_box.addLayout(grid1)
+
+ dwelllabel = QtGui.QLabel('Dwell:')
+ dwelllabel.setToolTip(
+ "Pause to allow the spindle to reach its\n"
+ "speed before cutting."
+ )
+ dwelltime = QtGui.QLabel('Duration [sec.]:')
+ dwelltime.setToolTip(
+ "Number of second to dwell."
+ )
+ self.dwell_cb = FCCheckBox()
+ self.dwelltime_entry = FCEntry()
+ grid1.addWidget(dwelllabel, 0, 0)
+ grid1.addWidget(self.dwell_cb, 0, 1)
+ grid1.addWidget(dwelltime, 1, 0)
+ grid1.addWidget(self.dwelltime_entry, 1, 1)
+
# GO Button
self.export_gcode_button = QtGui.QPushButton('Export G-Code')
self.export_gcode_button.setToolTip(
diff --git a/PlotCanvas.py b/PlotCanvas.py
index a933648..94469d2 100644
--- a/PlotCanvas.py
+++ b/PlotCanvas.py
@@ -12,26 +12,103 @@ from PyQt4 import QtGui, QtCore
from matplotlib import use as mpl_use
mpl_use("Qt4Agg")
-import FlatCAMApp
-import numpy as np
-import copy
-from math import ceil, floor
-import math
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureOffscreenCanvas
+from matplotlib.backends.backend_agg import FigureCanvasAgg
+import FlatCAMApp
+import logging
+
+log = logging.getLogger('base')
+
+
+class CanvasCache(QtCore.QObject):
+ """
+
+ Case story #1:
+
+ 1) No objects in the project.
+ 2) Object is created (new_object() emits object_created(obj)).
+ on_object_created() adds (i) object to collection and emits
+ (ii) new_object_available() then calls (iii) object.plot()
+ 3) object.plot() creates axes if necessary on
+ app.collection.figure. Then plots on it.
+ 4) Plots on a cache-size canvas (in background).
+ 5) Plot completes. Bitmap is generated.
+ 6) Visible canvas is painted.
+
+ """
+
+ # Signals:
+ # A bitmap is ready to be displayed.
+ new_screen = QtCore.pyqtSignal()
+
+ def __init__(self, plotcanvas, app, dpi=50):
+
+ super(CanvasCache, self).__init__()
+
+ self.app = app
+
+ self.plotcanvas = plotcanvas
+ self.dpi = dpi
+
+ self.figure = Figure(dpi=dpi)
+
+ self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
+ self.axes.set_frame_on(False)
+ self.axes.set_xticks([])
+ self.axes.set_yticks([])
+
+ self.canvas = FigureCanvasAgg(self.figure)
+
+ self.cache = None
+
+ def run(self):
+
+ log.debug("CanvasCache Thread Started!")
+
+ self.plotcanvas.update_screen_request.connect(self.on_update_req)
+
+ self.app.new_object_available.connect(self.on_new_object_available)
+
+ def on_update_req(self, extents):
+ """
+ Event handler for an updated display request.
+
+ :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
+ """
+
+ log.debug("Canvas update requested: %s" % str(extents))
+
+ # Note: This information below might be out of date. Establish
+ # a protocol regarding when to change the canvas in the main
+ # thread and when to check these values here in the background,
+ # or pass this data in the signal (safer).
+ log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
+ log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
+
+ # Move the requested screen portion to the main thread
+ # and inform about the update:
+
+ self.new_screen.emit()
+
+ # Continue to update the cache.
+
+ def on_new_object_available(self):
+
+ log.debug("A new object is available. Should plot it!")
+
class PlotCanvas(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
- app = None
- updates_queue = 0
+ # Signals:
+ # Request for new bitmap to display. The parameter
+ # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
+ update_screen_request = QtCore.pyqtSignal(list)
- image_ready = QtCore.pyqtSignal(object, tuple)
-
- def __init__(self, container):
+ def __init__(self, container, app):
"""
The constructor configures the Matplotlib figure that
will contain all plots, creates the base axes and connects
@@ -43,6 +120,8 @@ class PlotCanvas(QtCore.QObject):
super(PlotCanvas, self).__init__()
+ self.app = app
+
# Options
self.x_margin = 15 # pixels
self.y_margin = 25 # Pixels
@@ -54,24 +133,17 @@ class PlotCanvas(QtCore.QObject):
self.figure = Figure(dpi=50) # TODO: dpi needed?
self.figure.patch.set_visible(False)
- # Offscreen figure
- self.offscreen_figure = Figure(dpi=50)
- self.offscreen_figure.patch.set_visible(False)
-
# These axes show the ticks and grid. No plotting done here.
# New axes must have a label, otherwise mpl returns an existing one.
self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
self.axes.set_aspect(1)
self.axes.grid(True)
- # The canvas is the top level container (Gtk.DrawingArea)
+ # The canvas is the top level container (FigureCanvasQTAgg)
self.canvas = FigureCanvas(self.figure)
# self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
# self.canvas.setFocus()
- # Image
- self.image = None
-
#self.canvas.set_hexpand(1)
#self.canvas.set_vexpand(1)
#self.canvas.set_can_focus(True) # For key press
@@ -84,6 +156,15 @@ class PlotCanvas(QtCore.QObject):
# Update every time the canvas is re-drawn.
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
+ ### Bitmap Cache
+ self.cache = CanvasCache(self, self.app)
+ self.cache_thread = QtCore.QThread()
+ self.cache.moveToThread(self.cache_thread)
+ super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
+ # self.connect()
+ self.cache_thread.start()
+ self.cache.new_screen.connect(self.on_new_screen)
+
# Events
self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
@@ -103,11 +184,9 @@ class PlotCanvas(QtCore.QObject):
self.pan_axes = []
self.panning = False
- self.reset_nonupdate_bounds()
+ def on_new_screen(self):
- self.image_ready.connect(self.update_canvas)
-
- # self.plots_updated.connect(self.on_plots_updated)
+ log.debug("Cache updated the screen!")
def on_key_down(self, event):
"""
@@ -149,7 +228,7 @@ class PlotCanvas(QtCore.QObject):
def connect(self, event_name, callback):
"""
- Attach an event handler to the canvas through the native GTK interface.
+ Attach an event handler to the canvas through the native Qt interface.
:param event_name: Name of the event
:type event_name: str
@@ -159,12 +238,6 @@ class PlotCanvas(QtCore.QObject):
"""
self.canvas.connect(event_name, callback)
- def reset_nonupdate_bounds(self):
- self.bx1 = float('-inf')
- self.bx2 = float('inf')
- self.by1 = self.bx1
- self.by2 = self.bx2
-
def clear(self):
"""
Clears axes and figure.
@@ -176,7 +249,6 @@ class PlotCanvas(QtCore.QObject):
self.axes.cla()
try:
self.figure.clf()
- self.offscreen_figure.clf()
except KeyError:
FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
@@ -185,32 +257,8 @@ class PlotCanvas(QtCore.QObject):
self.axes.set_aspect(1)
self.axes.grid(True)
- # Prepare offscreen base axes
- ax = self.offscreen_figure.add_axes([0.0, 0.0, 1.0, 1.0], label='base')
- ax.set_frame_on(True)
- ax.patch.set_color("white")
- # Hide frame edge
- for spine in ax.spines:
- ax.spines[spine].set_visible(False)
- ax.set_aspect(1)
-
- # Set update bounds
- self.reset_nonupdate_bounds()
-
# Re-draw
- self.canvas.draw()
-
- def auto_adjust_axes(self, *args):
- """
- Calls ``adjust_axes()`` using the extents of the base axes.
-
- :rtype : None
- :return: None
- """
-
- xmin, xmax = self.axes.get_xlim()
- ymin, ymax = self.axes.get_ylim()
- self.adjust_axes(xmin, ymin, xmax, ymax)
+ self.canvas.draw_idle()
def adjust_axes(self, xmin, ymin, xmax, ymax):
"""
@@ -268,135 +316,21 @@ class PlotCanvas(QtCore.QObject):
# Sync re-draw to proper paint on form resize
self.canvas.draw()
- self.update()
- def update(self):
+ ##### Temporary place-holder for cached update #####
+ self.update_screen_request.emit([0, 0, 0, 0, 0])
- # Get objects collection bounds
- margin = 2
- x1, y1, x2, y2 = self.app.collection.get_bounds()
- x1, y1, x2, y2 = x1 - margin, y1 - margin, x2 + margin, y2 + margin
+ def auto_adjust_axes(self, *args):
+ """
+ Calls ``adjust_axes()`` using the extents of the base axes.
+
+ :rtype : None
+ :return: None
+ """
- # Get visible bounds
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
-
- # Truncate bounds
- print "Collection bounds", x1, x2, y1, y2
- print "Viewport", xmin, xmax, ymin, ymax
- if x1 < xmin or x2 > xmax or y1 < ymin or y2 > ymax:
- print "Truncating bounds"
- width = xmax - xmin
- height = ymax - ymin
-
- x1 = xmin - width * 2
- x2 = xmax + width * 2
- y1 = ymin - height * 2
- y2 = ymax + height * 2
-
- # self.bx1 = x1
- # self.bx2 = x2
- # self.by1 = y1
- # self.by2 = y2
-
- self.bx1 = xmin - width
- self.bx2 = xmax + width
- self.by1 = ymin - height
- self.by2 = ymax + height
- else:
- self.reset_nonupdate_bounds()
-
- # Calculate bounds in screen space
- points = self.axes.transData.transform([(x1, y1), (x2, y2)])
-
- # Round bounds to integers
- rounded_points = [(floor(points[0][0]), floor(points[0][1])), (ceil(points[1][0]), ceil(points[1][1]))]
-
- # Calculate width/height of image
- w, h = (rounded_points[1][0] - rounded_points[0][0]), (rounded_points[1][1] - rounded_points[0][1])
-
- # Get bounds back in axes units
- inverted_transform = self.axes.transData.inverted()
- bounds = inverted_transform.transform(rounded_points)
-
- # print "image bounds", x1, x2, y1, y2, points, rounded_points, bounds, w, h, self.axes.transData.transform(bounds)
-
- x1, x2, y1, y2 = bounds[0][0], bounds[1][0], bounds[0][1], bounds[1][1]
-
- # print "new image bounds", x1, x2, y1, y2
-
- pixel = inverted_transform.transform([(0, 0), (1, 1)])
- pixel_size = pixel[1][0] - pixel[0][0]
-
- # print "pixel size", pixel, pixel_size
-
- def update_image(figure):
-
- # Abort update if next update in queue
- if self.updates_queue > 1:
- self.updates_queue -= 1
- return
-
- # Rescale axes
- for ax in figure.get_axes():
- ax.set_xlim(x1 + pixel_size, x2 + pixel_size)
- ax.set_ylim(y1, y2)
- ax.set_xticks([])
- ax.set_yticks([])
-
- # Resize figure
- dpi = figure.dpi
- figure.set_size_inches(w / dpi, h / dpi)
-
- try:
- # Draw to buffer
- self.updates_queue -= 1
- offscreen_canvas = FigureOffscreenCanvas(figure)
- offscreen_canvas.draw()
-
- # Abort drawing if next update in queue
- if self.updates_queue > 0:
- del offscreen_canvas
- return
-
- buf = offscreen_canvas.buffer_rgba()
- ncols, nrows = offscreen_canvas.get_width_height()
- image = np.frombuffer(buf, dtype=np.uint8).reshape(nrows, ncols, 4)
-
- self.image_ready.emit(copy.deepcopy(image), (x1, x2, y1, y2))
-
- del image
- del offscreen_canvas
-
- except Exception as e:
- self.app.log.debug(e.message)
-
- # Do job in background
- proc = self.app.proc_container.new("Updating view")
-
- def job_thread(app_obj, figure):
- try:
- update_image(figure)
- except Exception as e:
- proc.done()
- raise e
- proc.done()
-
- # self.app.inform.emit("View update starting ...")
- self.updates_queue += 1
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app, self.offscreen_figure]})
-
- def update_canvas(self, image, bounds):
-
- try:
- self.image.remove()
- except:
- pass
-
- self.image = self.axes.imshow(image, extent=bounds, interpolation="Nearest")
- self.canvas.draw_idle()
-
- del image
+ self.adjust_axes(xmin, ymin, xmax, ymax)
def zoom(self, factor, center=None):
"""
@@ -412,7 +346,6 @@ class PlotCanvas(QtCore.QObject):
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
-
width = xmax - xmin
height = ymax - ymin
@@ -437,10 +370,10 @@ class PlotCanvas(QtCore.QObject):
ax.set_ylim((ymin, ymax))
# Async re-draw
- # self.canvas.draw_idle()
-
self.canvas.draw_idle()
- self.update()
+
+ ##### Temporary place-holder for cached update #####
+ self.update_screen_request.emit([0, 0, 0, 0, 0])
def pan(self, x, y):
xmin, xmax = self.axes.get_xlim()
@@ -456,6 +389,9 @@ class PlotCanvas(QtCore.QObject):
# Re-draw
self.canvas.draw_idle()
+ ##### Temporary place-holder for cached update #####
+ self.update_screen_request.emit([0, 0, 0, 0, 0])
+
def new_axes(self, name):
"""
Creates and returns an Axes object attached to this object's Figure.
@@ -465,13 +401,7 @@ class PlotCanvas(QtCore.QObject):
:rtype: Axes
"""
- ax = self.offscreen_figure.add_axes([0.0, 0.0, 1.0, 1.0], label=name)
-
- ax.set_frame_on(False) # No frame
- ax.patch.set_visible(False) # No background
- ax.set_aspect(1)
-
- return ax
+ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
def on_scroll(self, event):
"""
@@ -526,8 +456,7 @@ class PlotCanvas(QtCore.QObject):
self.pan_axes.append(a)
# Set pan view flag
- if len(self.pan_axes) > 0:
- self.panning = True;
+ if len(self.pan_axes) > 0: self.panning = True;
def on_mouse_release(self, event):
@@ -553,19 +482,43 @@ class PlotCanvas(QtCore.QObject):
for a in self.pan_axes:
a.drag_pan(1, event.key, event.x, event.y)
- # Update
- xmin, xmax = self.axes.get_xlim()
- ymin, ymax = self.axes.get_ylim()
+ # Async re-draw (redraws only on thread idle state, uses timer on backend)
+ self.canvas.draw_idle()
- if xmin < self.bx1 or xmax > self.bx2 or ymin < self.by1 or ymax > self.by2:
- # Redraw image
- print "Redrawing image"
- self.update()
- else:
- # Async re-draw (redraws only on thread idle state, uses timer on backend)
- self.canvas.draw_idle()
+ ##### Temporary place-holder for cached update #####
+ self.update_screen_request.emit([0, 0, 0, 0, 0])
def on_draw(self, renderer):
# Store background on canvas redraw
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
+
+ def get_axes_pixelsize(self):
+ """
+ Axes size in pixels.
+
+ :return: Pixel width and height
+ :rtype: tuple
+ """
+ bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
+ width, height = bbox.width, bbox.height
+ width *= self.figure.dpi
+ height *= self.figure.dpi
+ return width, height
+
+ def get_density(self):
+ """
+ Returns unit length per pixel on horizontal
+ and vertical axes.
+
+ :return: X and Y density
+ :rtype: tuple
+ """
+ xpx, ypx = self.get_axes_pixelsize()
+
+ xmin, xmax = self.axes.get_xlim()
+ ymin, ymax = self.axes.get_ylim()
+ width = xmax - xmin
+ height = ymax - ymin
+
+ return width / xpx, height / ypx
diff --git a/README.md b/README.md
index f54a382..c150ae4 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,8 @@
FlatCAM: 2D Computer-Aided PCB Manufacturing
============================================
-(c) 2014-2015 Juan Pablo Caram
+(c) 2014-2016 Juan Pablo Caram
FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
Among other things, it can take a Gerber file generated by your favorite PCB
-CAD program, and create G-Code for Isolation routing.
-
-============================================
-
-This fork features:
-
-- "Clear non-copper" feature, supporting multi-tool work.
-- Groups in Project view.
-- Pan view by dragging in visualizer window with pressed MMB.
-- Basic visualizer performance tricks.
-
-Plans for the far future:
-
-- OpenGL-based visualizer.
-
-Some screenshots:
-
-
-
-
\ No newline at end of file
+CAD program, and create G-Code for Isolation routing.
\ No newline at end of file
diff --git a/camlib.py b/camlib.py
index c91c59d..169a726 100644
--- a/camlib.py
+++ b/camlib.py
@@ -9,6 +9,7 @@
#from scipy import optimize
#import traceback
+from cStringIO import StringIO
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
transpose
from numpy.linalg import solve, norm
@@ -16,6 +17,7 @@ from matplotlib.figure import Figure
import re
import sys
import traceback
+from decimal import Decimal
import collections
import numpy as np
@@ -42,6 +44,16 @@ import simplejson as json
# TODO: Commented for FlatCAM packaging with cx_freeze
#from matplotlib.pyplot import plot, subplot
+import xml.etree.ElementTree as ET
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+import itertools
+
+import xml.etree.ElementTree as ET
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+
+
+from svgparse import *
+
import logging
log = logging.getLogger('base2')
@@ -125,6 +137,60 @@ 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 is_empty(self):
+
+ if isinstance(self.solid_geometry, BaseGeometry):
+ return self.solid_geometry.is_empty
+
+ if isinstance(self.solid_geometry, list):
+ return len(self.solid_geometry) == 0
+
+ raise Exception("self.solid_geometry is neither BaseGeometry or list.")
+
+ 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.
+
+ :param points: The vertices of the polygon.
+ :return: none
+ """
+ if self.solid_geometry is None:
+ self.solid_geometry = []
+
+ #pathonly should be allways True, otherwise polygons are not subtracted
+ flat_geometry = self.flatten(pathonly=True)
+ log.debug("%d paths" % len(flat_geometry))
+ polygon=Polygon(points)
+ toolgeo=cascaded_union(polygon)
+ diffs=[]
+ for target in flat_geometry:
+ if type(target) == LineString or type(target) == LinearRing:
+ diffs.append(target.difference(toolgeo))
+ else:
+ log.warning("Not implemented.")
+ self.solid_geometry=cascaded_union(diffs)
+
def bounds(self):
"""
Returns coordinates of rectangular bounds
@@ -193,7 +259,6 @@ class Geometry(object):
return interiors
-
def get_exteriors(self, geometry=None):
"""
Returns all exteriors of polygons in geometry. Uses
@@ -335,14 +400,39 @@ class Geometry(object):
"""
return self.solid_geometry.buffer(offset)
- def is_empty(self):
+ def import_svg(self, filename, flip=True):
+ """
+ Imports shapes from an SVG file into the object's geometry.
+
+ :param filename: Path to the SVG file.
+ :type filename: str
+ :return: None
+ """
+
+ # Parse into list of shapely objects
+ svg_tree = ET.parse(filename)
+ svg_root = svg_tree.getroot()
+
+ # Change origin to bottom left
+ # h = float(svg_root.get('height'))
+ # w = float(svg_root.get('width'))
+ h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
+ geos = getsvggeo(svg_root)
+
+ if flip:
+ geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
+
+ # Add to object
if self.solid_geometry is None:
- return True
+ self.solid_geometry = []
- if type(self.solid_geometry) is list and len(self.solid_geometry) == 0:
- return True
+ if type(self.solid_geometry) is list:
+ self.solid_geometry.append(cascaded_union(geos))
+ else: # It's shapely geometry
+ self.solid_geometry = cascaded_union([self.solid_geometry,
+ cascaded_union(geos)])
- return False
+ return
def size(self):
"""
@@ -802,6 +892,47 @@ 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
+
+ def mirror(self, axis, point):
+ """
+ Mirrors the object around a specified axis passign through
+ the given point.
+
+ :param axis: "X" or "Y" indicates around which axis to mirror.
+ :type axis: str
+ :param point: [x, y] point belonging to the mirror axis.
+ :type point: list
+ :return: None
+ """
+
+ px, py = point
+ xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+ ## solid_geometry ???
+ # It's a cascaded union of objects.
+ self.solid_geometry = affinity.scale(self.solid_geometry,
+ xscale, yscale, origin=(px, py))
+
class ApertureMacro:
"""
@@ -1125,6 +1256,8 @@ class ApertureMacro:
:param modifiers: Modifiers (parameters) for this macro
:type modifiers: list
+ :return: Shapely geometry
+ :rtype: shapely.geometry.polygon
"""
## Primitive makers
@@ -1145,11 +1278,11 @@ class ApertureMacro:
modifiers = [float(m) for m in modifiers]
self.locvars = {}
for i in range(0, len(modifiers)):
- self.locvars[str(i+1)] = modifiers[i]
+ self.locvars[str(i + 1)] = modifiers[i]
## Parse
self.primitives = [] # Cleanup
- self.geometry = None
+ self.geometry = Polygon()
self.parse_content()
## Make the geometry
@@ -1158,9 +1291,9 @@ class ApertureMacro:
prim_geo = makers[str(int(primitive[0]))](primitive[1:])
# Add it (according to polarity)
- if self.geometry is None and prim_geo['pol'] == 1:
- self.geometry = prim_geo['geometry']
- continue
+ # if self.geometry is None and prim_geo['pol'] == 1:
+ # self.geometry = prim_geo['geometry']
+ # continue
if prim_geo['pol'] == 1:
self.geometry = self.geometry.union(prim_geo['geometry'])
continue
@@ -1263,7 +1396,9 @@ class Gerber (Geometry):
self.comm_re = re.compile(r'^G0?4(.*)$')
# AD - Aperture definition
- self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.]*)(?:,(.*))?\*%$')
+ # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
+ # NOTE: Adding "-" to support output from Upverter.
+ self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
# AM - Aperture Macro
# Beginning of macro (Ends with *%):
@@ -1383,35 +1518,35 @@ class Gerber (Geometry):
## Solid geometry
self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
- def mirror(self, axis, point):
- """
- Mirrors the object around a specified axis passign through
- the given point. What is affected:
-
- * ``buffered_paths``
- * ``flash_geometry``
- * ``solid_geometry``
- * ``regions``
-
- NOTE:
- Does not modify the data used to create these elements. If these
- are recreated, the scaling will be lost. This behavior was modified
- because of the complexity reached in this class.
-
- :param axis: "X" or "Y" indicates around which axis to mirror.
- :type axis: str
- :param point: [x, y] point belonging to the mirror axis.
- :type point: list
- :return: None
- """
-
- px, py = point
- xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
-
- ## solid_geometry ???
- # It's a cascaded union of objects.
- self.solid_geometry = affinity.scale(self.solid_geometry,
- xscale, yscale, origin=(px, py))
+ # def mirror(self, axis, point):
+ # """
+ # Mirrors the object around a specified axis passign through
+ # the given point. What is affected:
+ #
+ # * ``buffered_paths``
+ # * ``flash_geometry``
+ # * ``solid_geometry``
+ # * ``regions``
+ #
+ # NOTE:
+ # Does not modify the data used to create these elements. If these
+ # are recreated, the scaling will be lost. This behavior was modified
+ # because of the complexity reached in this class.
+ #
+ # :param axis: "X" or "Y" indicates around which axis to mirror.
+ # :type axis: str
+ # :param point: [x, y] point belonging to the mirror axis.
+ # :type point: list
+ # :return: None
+ # """
+ #
+ # px, py = point
+ # xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+ #
+ # ## solid_geometry ???
+ # # It's a cascaded union of objects.
+ # self.solid_geometry = affinity.scale(self.solid_geometry,
+ # xscale, yscale, origin=(px, py))
def aperture_parse(self, apertureId, apertureType, apParameters):
"""
@@ -1521,7 +1656,8 @@ class Gerber (Geometry):
# Otherwise leave as is.
else:
- yield cleanline
+ # yield cleanline
+ yield line
break
self.parse_lines(line_generator(), follow=follow)
@@ -1602,7 +1738,7 @@ class Gerber (Geometry):
match = self.am1_re.search(gline)
# Start macro if match, else not an AM, carry on.
if match:
- log.info("Starting macro. Line %d: %s" % (line_num, gline))
+ log.debug("Starting macro. Line %d: %s" % (line_num, gline))
current_macro = match.group(1)
self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
if match.group(2): # Append
@@ -1610,13 +1746,13 @@ class Gerber (Geometry):
if match.group(3): # Finish macro
#self.aperture_macros[current_macro].parse_content()
current_macro = None
- log.info("Macro complete in 1 line.")
+ log.debug("Macro complete in 1 line.")
continue
else: # Continue macro
- log.info("Continuing macro. Line %d." % line_num)
+ log.debug("Continuing macro. Line %d." % line_num)
match = self.am2_re.search(gline)
if match: # Finish macro
- log.info("End of macro. Line %d." % line_num)
+ log.debug("End of macro. Line %d." % line_num)
self.aperture_macros[current_macro].append(match.group(1))
#self.aperture_macros[current_macro].parse_content()
current_macro = None
@@ -1659,7 +1795,10 @@ class Gerber (Geometry):
## --- BUFFERED ---
if making_region:
- geo = Polygon(path)
+ if follow:
+ geo = Polygon()
+ else:
+ geo = Polygon(path)
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
@@ -1669,7 +1808,9 @@ class Gerber (Geometry):
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
- if not geo.is_empty: poly_buffer.append(geo)
+
+ if not geo.is_empty:
+ poly_buffer.append(geo)
path = [[current_x, current_y]] # Start new path
@@ -1681,17 +1822,26 @@ class Gerber (Geometry):
if len(path) > 1:
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
- geo = LineString(path).buffer(width / 2)
- if not geo.is_empty: poly_buffer.append(geo)
+
+ if follow:
+ geo = LineString(path)
+ else:
+ geo = LineString(path).buffer(width / 2)
+
+ if not geo.is_empty:
+ poly_buffer.append(geo)
# Reset path starting point
path = [[current_x, current_y]]
# --- BUFFERED ---
# Draw the flash
+ if follow:
+ continue
flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
self.apertures[current_aperture])
- if not flash.is_empty: poly_buffer.append(flash)
+ if not flash.is_empty:
+ poly_buffer.append(flash)
continue
@@ -1744,8 +1894,13 @@ class Gerber (Geometry):
# --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"]
- buffered = LineString(path).buffer(width / 2)
- if not buffered.is_empty: poly_buffer.append(buffered)
+
+ if follow:
+ buffered = LineString(path)
+ else:
+ buffered = LineString(path).buffer(width / 2)
+ if not buffered.is_empty:
+ poly_buffer.append(buffered)
current_x = x
current_y = y
@@ -1858,9 +2013,12 @@ class Gerber (Geometry):
log.debug("Bare op-code %d." % current_operation_code)
# flash = Gerber.create_flash_geometry(Point(path[-1]),
# self.apertures[current_aperture])
+ if follow:
+ continue
flash = Gerber.create_flash_geometry(Point(current_x, current_y),
self.apertures[current_aperture])
- if not flash.is_empty: poly_buffer.append(flash)
+ if not flash.is_empty:
+ poly_buffer.append(flash)
except IndexError:
log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
@@ -1882,8 +2040,13 @@ class Gerber (Geometry):
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
- geo = LineString(path).buffer(width/2)
- if not geo.is_empty: poly_buffer.append(geo)
+
+ if follow:
+ geo = LineString(path)
+ else:
+ geo = LineString(path).buffer(width/2)
+ if not geo.is_empty:
+ poly_buffer.append(geo)
path = [path[-1]]
@@ -1910,10 +2073,15 @@ class Gerber (Geometry):
# "aperture": last_path_aperture})
# --- Buffered ---
- region = Polygon(path)
+ if follow:
+ region = Polygon()
+ else:
+ region = Polygon(path)
if not region.is_valid:
- region = region.buffer(0)
- if not region.is_empty: poly_buffer.append(region)
+ if not follow:
+ region = region.buffer(0)
+ if not region.is_empty:
+ poly_buffer.append(region)
path = [[current_x, current_y]] # Start new path
continue
@@ -1946,8 +2114,14 @@ class Gerber (Geometry):
if len(path) > 1:
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
- geo = LineString(path).buffer(width / 2)
- if not geo.is_empty: poly_buffer.append(geo)
+
+ if follow:
+ geo = LineString(path)
+ else:
+ geo = LineString(path).buffer(width / 2)
+ if not geo.is_empty:
+ poly_buffer.append(geo)
+
path = [path[-1]]
continue
@@ -1962,8 +2136,13 @@ class Gerber (Geometry):
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
- geo = LineString(path).buffer(width / 2)
- if not geo.is_empty: poly_buffer.append(geo)
+
+ if follow:
+ geo = LineString(path)
+ else:
+ geo = LineString(path).buffer(width / 2)
+ if not geo.is_empty:
+ poly_buffer.append(geo)
path = [path[-1]]
@@ -2034,10 +2213,18 @@ class Gerber (Geometry):
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
- geo = LineString(path).buffer(width / 2)
- if not geo.is_empty: poly_buffer.append(geo)
+ if follow:
+ geo = LineString(path)
+ else:
+ geo = LineString(path).buffer(width / 2)
+ if not geo.is_empty:
+ poly_buffer.append(geo)
# --- Apply buffer ---
+ if follow:
+ self.solid_geometry = poly_buffer
+ return
+
log.warn("Joining %d polygons." % len(poly_buffer))
if self.use_buffer_for_union:
log.debug("Union by buffer...")
@@ -2117,8 +2304,11 @@ class Gerber (Geometry):
if aperture['type'] == 'AM': # Aperture Macro
loc = location.coords[0]
flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
+ if flash_geo.is_empty:
+ log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
+ log.warning("Unknown aperture type: %s" % aperture['type'])
return None
def create_geometry(self):
@@ -2604,7 +2794,8 @@ class CNCjob(Geometry):
self.feedrate = feedrate
self.tooldia = tooldia
self.unitcode = {"IN": "G20", "MM": "G21"}
- self.pausecode = "G04 P1"
+ # TODO: G04 Does not exist. It's G4 and now we are handling in postprocessing.
+ #self.pausecode = "G04 P1"
self.feedminutecode = "G94"
self.absolutecode = "G90"
self.gcode = ""
@@ -2656,12 +2847,20 @@ class CNCjob(Geometry):
log.debug("Creating CNC Job from Excellon...")
# Tools
+
+ # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
+ # so we actually are sorting the tools by diameter
+ sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
if tools == "all":
- tools = [tool for tool in exobj.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:
- tools = [x.strip() for x in tools.split(",")]
- tools = filter(lambda i: i in exobj.tools, tools)
- log.debug("Tools are: %s" % str(tools))
+ selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ','
+ selected_tools = filter(lambda i: i in selected_tools, selected_tools)
+
+ # Create a sorted list of selected tools from the sorted_tools list
+ tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
+ log.debug("Tools selected and sorted are: %s" % str(tools))
# Points (Group by tool)
points = {}
@@ -2678,7 +2877,8 @@ class CNCjob(Geometry):
# Basic G-Code macros
t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n"
down = "G01 Z%.4f\n" % self.z_cut
- up = "G01 Z%.4f\n" % self.z_move
+ up = "G00 Z%.4f\n" % self.z_move
+ up_to_zero = "G01 Z0\n"
# Initialization
gcode = self.unitcode[self.units.upper()] + "\n"
@@ -2688,32 +2888,36 @@ class CNCjob(Geometry):
gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
if self.spindlespeed is not None:
- gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
+ # Spindle start with configured speed
+ gcode += "M03 S%d\n" % int(self.spindlespeed)
else:
gcode += "M03\n" # Spindle start
- gcode += self.pausecode + "\n"
+ #gcode += self.pausecode + "\n"
- for tool in points:
+ 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 has points.
+ 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:
+ # Spindle start with configured speed
+ gcode += "M03 S%d\n" % int(self.spindlespeed)
+ 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_to_zero + up
gcode += t % (0, 0)
gcode += "M05\n" # Spindle stop
@@ -2786,7 +2990,7 @@ class CNCjob(Geometry):
self.gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
else:
self.gcode += "M03\n" # Spindle start
- self.gcode += self.pausecode + "\n"
+ #self.gcode += self.pausecode + "\n"
## Iterate over geometry paths getting the nearest each time.
log.debug("Starting G-Code...")
@@ -2822,17 +3026,24 @@ class CNCjob(Geometry):
#--------- Multi-pass ---------
else:
+ if isinstance(self.z_cut, Decimal):
+ z_cut = self.z_cut
+ else:
+ z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001'))
+
if depthpercut is None:
- depthpercut = self.z_cut
+ depthpercut = z_cut
+ elif not isinstance(depthpercut, Decimal):
+ depthpercut = Decimal(depthpercut).quantize(Decimal('0.000000001'))
depth = 0
reverse = False
- while depth > self.z_cut:
+ while depth > z_cut:
# Increase depth. Limit to z_cut.
depth -= depthpercut
- if depth < self.z_cut:
- depth = self.z_cut
+ if depth < z_cut:
+ depth = z_cut
# Cut at specific depth and do not lift the tool.
# Note: linear2gcode() will use G00 to move to the
@@ -2887,65 +3098,25 @@ class CNCjob(Geometry):
self.gcode += "G00 X0Y0\n"
self.gcode += "M05\n" # Spindle stop
- def pre_parse(self, gtext):
+ @staticmethod
+ def codes_split(gline):
"""
- Separates parts of the G-Code text into a list of dictionaries.
- Used by ``self.gcode_parse()``.
+ Parses a line of G-Code such as "G01 X1234 Y987" into
+ a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
- :param gtext: A single string with g-code
+ :param gline: G-Code line string
+ :return: Dictionary with parsed line.
"""
- # Units: G20-inches, G21-mm
- units_re = re.compile(r'^G2([01])')
+ command = {}
- # TODO: This has to be re-done
- gcmds = []
- lines = gtext.split("\n") # TODO: This is probably a lot of work!
- for line in lines:
- # Clean up
- line = line.strip()
+ match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
+ while match:
+ command[match.group(1)] = float(match.group(2).replace(" ", ""))
+ gline = gline[match.end():]
+ match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
- # Remove comments
- # NOTE: Limited to 1 bracket pair
- op = line.find("(")
- cl = line.find(")")
- #if op > -1 and cl > op:
- if cl > op > -1:
- #comment = line[op+1:cl]
- line = line[:op] + line[(cl+1):]
-
- # Units
- match = units_re.match(line)
- if match:
- self.units = {'0': "IN", '1': "MM"}[match.group(1)]
-
- # Parse GCode
- # 0 4 12
- # G01 X-0.007 Y-0.057
- # --> codes_idx = [0, 4, 12]
- codes = "NMGXYZIJFPST"
- codes_idx = []
- i = 0
- for ch in line:
- if ch in codes:
- codes_idx.append(i)
- i += 1
- n_codes = len(codes_idx)
- if n_codes == 0:
- continue
-
- # Separate codes in line
- parts = []
- for p in range(n_codes - 1):
- parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
- parts.append(line[codes_idx[-1]:].strip())
-
- # Separate codes from values
- cmds = {}
- for part in parts:
- cmds[part[0]] = float(part[1:])
- gcmds.append(cmds)
- return gcmds
+ return command
def gcode_parse(self):
"""
@@ -2958,10 +3129,7 @@ class CNCjob(Geometry):
# Results go here
geometry = []
-
- # TODO: Merge into single parser?
- gobjs = self.pre_parse(self.gcode)
-
+
# Last known instruction
current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
@@ -2970,7 +3138,14 @@ class CNCjob(Geometry):
path = [(0, 0)]
# Process every instruction
- for gobj in gobjs:
+ for line in StringIO(self.gcode):
+
+ gobj = self.codes_split(line)
+
+ ## Units
+ if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
+ self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
+ continue
## Changing height
if 'Z' in gobj:
@@ -3014,7 +3189,7 @@ class CNCjob(Geometry):
center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
radius = sqrt(gobj['I']**2 + gobj['J']**2)
start = arctan2(-gobj['J'], -gobj['I'])
- stop = arctan2(-center[1]+y, -center[0]+x)
+ stop = arctan2(-center[1] + y, -center[0] + x)
path += arc(center, radius, start, stop,
arcdir[current['G']],
self.steps_per_circ)
@@ -3226,6 +3401,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/camlib.pyc b/camlib.pyc
index 9005553..c42a4ee 100644
Binary files a/camlib.pyc and b/camlib.pyc differ
diff --git a/descartes/__init__.pyc b/descartes/__init__.pyc
index e763cc2..480e4df 100644
Binary files a/descartes/__init__.pyc and b/descartes/__init__.pyc differ
diff --git a/descartes/patch.pyc b/descartes/patch.pyc
index 66d08b5..8624925 100644
Binary files a/descartes/patch.pyc and b/descartes/patch.pyc differ
diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh
index cc9aae9..bb1d7cc 100755
--- a/setup_ubuntu.sh
+++ b/setup_ubuntu.sh
@@ -13,3 +13,4 @@ pip install --upgrade matplotlib
pip install --upgrade Shapely
apt-get install libspatialindex-dev
pip install rtree
+pip install svg.path
\ No newline at end of file
diff --git a/svgparse.py b/svgparse.py
new file mode 100644
index 0000000..544f0ba
--- /dev/null
+++ b/svgparse.py
@@ -0,0 +1,522 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing #
+# http://flatcam.org #
+# Author: Juan Pablo Caram (c) #
+# Date: 12/18/2015 #
+# MIT Licence #
+# #
+# SVG Features supported: #
+# * Groups #
+# * Rectangles (w/ rounded corners) #
+# * Circles #
+# * Ellipses #
+# * Polygons #
+# * Polylines #
+# * Lines #
+# * Paths #
+# * All transformations #
+# #
+# Reference: www.w3.org/TR/SVG/Overview.html #
+############################################################
+
+import xml.etree.ElementTree as ET
+import re
+import itertools
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+from shapely.geometry import LinearRing, LineString, Point
+from shapely.affinity import translate, rotate, scale, skew, affine_transform
+import numpy as np
+import logging
+
+log = logging.getLogger('base2')
+
+
+def svgparselength(lengthstr):
+ """
+ Parse an SVG length string into a float and a units
+ string, if any.
+
+ :param lengthstr: SVG length string.
+ :return: Number and units pair.
+ :rtype: tuple(float, str|None)
+ """
+
+ integer_re_str = r'[+-]?[0-9]+'
+ number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
+ r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
+ length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
+
+ match = re.search(length_re_str, lengthstr)
+ if match:
+ return float(match.group(1)), match.group(2)
+
+ raise Exception('Cannot parse SVG length: %s' % lengthstr)
+
+
+def path2shapely(path, res=1.0):
+ """
+ Converts an svg.path.Path into a Shapely
+ LinearRing or LinearString.
+
+ :rtype : LinearRing
+ :rtype : LineString
+ :param path: svg.path.Path instance
+ :param res: Resolution (minimum step along path)
+ :return: Shapely geometry object
+ """
+
+ points = []
+
+ for component in path:
+
+ # Line
+ if isinstance(component, Line):
+ start = component.start
+ x, y = start.real, start.imag
+ if len(points) == 0 or points[-1] != (x, y):
+ points.append((x, y))
+ end = component.end
+ points.append((end.real, end.imag))
+ continue
+
+ # Arc, CubicBezier or QuadraticBezier
+ if isinstance(component, Arc) or \
+ isinstance(component, CubicBezier) or \
+ isinstance(component, QuadraticBezier):
+
+ # How many points to use in the dicrete representation.
+ length = component.length(res / 10.0)
+ steps = int(length / res + 0.5)
+
+ # solve error when step is below 1,
+ # it may cause other problems, but LineString needs at least two points
+ if steps == 0:
+ steps = 1
+
+ frac = 1.0 / steps
+
+ # print length, steps, frac
+ for i in range(steps):
+ point = component.point(i * frac)
+ x, y = point.real, point.imag
+ if len(points) == 0 or points[-1] != (x, y):
+ points.append((x, y))
+ end = component.point(1.0)
+ points.append((end.real, end.imag))
+ continue
+
+ log.warning("I don't know what this is:", component)
+ continue
+
+ if path.closed:
+ return LinearRing(points)
+ else:
+ return LineString(points)
+
+
+def svgrect2shapely(rect, n_points=32):
+ """
+ Converts an SVG rect into Shapely geometry.
+
+ :param rect: Rect Element
+ :type rect: xml.etree.ElementTree.Element
+ :return: shapely.geometry.polygon.LinearRing
+ """
+ w = svgparselength(rect.get('width'))[0]
+ h = svgparselength(rect.get('height'))[0]
+ x_obj = rect.get('x')
+ if x_obj is not None:
+ x = svgparselength(x_obj)[0]
+ else:
+ x = 0
+ y_obj = rect.get('y')
+ if y_obj is not None:
+ y = svgparselength(y_obj)[0]
+ else:
+ y = 0
+ rxstr = rect.get('rx')
+ rystr = rect.get('ry')
+
+ if rxstr is None and rystr is None: # Sharp corners
+ pts = [
+ (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
+ ]
+
+ else: # Rounded corners
+ rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
+ ry = 0.0 if rystr is None else svgparselength(rystr)[0]
+
+ n_points = int(n_points / 4 + 0.5)
+ t = np.arange(n_points, dtype=float) / n_points / 4
+
+ x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
+ y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
+
+ lower_right = [(x_[i], y_[i]) for i in range(n_points)]
+
+ x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
+ y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
+
+ upper_right = [(x_[i], y_[i]) for i in range(n_points)]
+
+ x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
+ y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
+
+ upper_left = [(x_[i], y_[i]) for i in range(n_points)]
+
+ x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
+ y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
+
+ lower_left = [(x_[i], y_[i]) for i in range(n_points)]
+
+ pts = [(x + rx, y), (x - rx + w, y)] + \
+ lower_right + \
+ [(x + w, y + ry), (x + w, y + h - ry)] + \
+ upper_right + \
+ [(x + w - rx, y + h), (x + rx, y + h)] + \
+ upper_left + \
+ [(x, y + h - ry), (x, y + ry)] + \
+ lower_left
+
+ return LinearRing(pts)
+
+
+def svgcircle2shapely(circle):
+ """
+ Converts an SVG circle into Shapely geometry.
+
+ :param circle: Circle Element
+ :type circle: xml.etree.ElementTree.Element
+ :return: Shapely representation of the circle.
+ :rtype: shapely.geometry.polygon.LinearRing
+ """
+ # cx = float(circle.get('cx'))
+ # cy = float(circle.get('cy'))
+ # r = float(circle.get('r'))
+ cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
+ cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
+ r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
+
+ # TODO: No resolution specified.
+ return Point(cx, cy).buffer(r)
+
+
+def svgellipse2shapely(ellipse, n_points=64):
+ """
+ Converts an SVG ellipse into Shapely geometry
+
+ :param ellipse: Ellipse Element
+ :type ellipse: xml.etree.ElementTree.Element
+ :param n_points: Number of discrete points in output.
+ :return: Shapely representation of the ellipse.
+ :rtype: shapely.geometry.polygon.LinearRing
+ """
+
+ cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
+ cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
+
+ rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
+ ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
+
+ t = np.arange(n_points, dtype=float) / n_points
+ x = cx + rx * np.cos(2 * np.pi * t)
+ y = cy + ry * np.sin(2 * np.pi * t)
+ pts = [(x[i], y[i]) for i in range(n_points)]
+
+ return LinearRing(pts)
+
+
+def svgline2shapely(line):
+ """
+
+ :param line: Line element
+ :type line: xml.etree.ElementTree.Element
+ :return: Shapely representation on the line.
+ :rtype: shapely.geometry.polygon.LinearRing
+ """
+
+ x1 = svgparselength(line.get('x1'))[0]
+ y1 = svgparselength(line.get('y1'))[0]
+ x2 = svgparselength(line.get('x2'))[0]
+ y2 = svgparselength(line.get('y2'))[0]
+
+ return LineString([(x1, y1), (x2, y2)])
+
+
+def svgpolyline2shapely(polyline):
+
+ ptliststr = polyline.get('points')
+ points = parse_svg_point_list(ptliststr)
+
+ return LineString(points)
+
+
+def svgpolygon2shapely(polygon):
+
+ ptliststr = polygon.get('points')
+ points = parse_svg_point_list(ptliststr)
+
+ return LinearRing(points)
+
+
+def getsvggeo(node):
+ """
+ Extracts and flattens all geometry from an SVG node
+ into a list of Shapely geometry.
+
+ :param node: xml.etree.ElementTree.Element
+ :return: List of Shapely geometry
+ :rtype: list
+ """
+ kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
+ geo = []
+
+ # Recurse
+ if len(node) > 0:
+ for child in node:
+ subgeo = getsvggeo(child)
+ if subgeo is not None:
+ geo += subgeo
+
+ # Parse
+ elif kind == 'path':
+ log.debug("***PATH***")
+ P = parse_path(node.get('d'))
+ P = path2shapely(P)
+ geo = [P]
+
+ elif kind == 'rect':
+ log.debug("***RECT***")
+ R = svgrect2shapely(node)
+ geo = [R]
+
+ elif kind == 'circle':
+ log.debug("***CIRCLE***")
+ C = svgcircle2shapely(node)
+ geo = [C]
+
+ elif kind == 'ellipse':
+ log.debug("***ELLIPSE***")
+ E = svgellipse2shapely(node)
+ geo = [E]
+
+ elif kind == 'polygon':
+ log.debug("***POLYGON***")
+ poly = svgpolygon2shapely(node)
+ geo = [poly]
+
+ elif kind == 'line':
+ log.debug("***LINE***")
+ line = svgline2shapely(node)
+ geo = [line]
+
+ elif kind == 'polyline':
+ log.debug("***POLYLINE***")
+ pline = svgpolyline2shapely(node)
+ geo = [pline]
+
+ else:
+ log.warning("Unknown kind: " + kind)
+ geo = None
+
+ # ignore transformation for unknown kind
+ if geo is not None:
+ # Transformations
+ if 'transform' in node.attrib:
+ trstr = node.get('transform')
+ trlist = parse_svg_transform(trstr)
+ #log.debug(trlist)
+
+ # Transformations are applied in reverse order
+ for tr in trlist[::-1]:
+ if tr[0] == 'translate':
+ geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
+ elif tr[0] == 'scale':
+ geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
+ for geoi in geo]
+ elif tr[0] == 'rotate':
+ geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
+ for geoi in geo]
+ elif tr[0] == 'skew':
+ geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
+ for geoi in geo]
+ elif tr[0] == 'matrix':
+ geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
+ else:
+ raise Exception('Unknown transformation: %s', tr)
+
+ return geo
+
+
+def parse_svg_point_list(ptliststr):
+ """
+ Returns a list of coordinate pairs extracted from the "points"
+ attribute in SVG polygons and polylines.
+
+ :param ptliststr: "points" attribute string in polygon or polyline.
+ :return: List of tuples with coordinates.
+ """
+
+ pairs = []
+ last = None
+ pos = 0
+ i = 0
+
+ for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
+
+ val = float(ptliststr[pos:match.start()])
+
+ if i % 2 == 1:
+ pairs.append((last, val))
+ else:
+ last = val
+
+ pos = match.end()
+ i += 1
+
+ # Check for last element
+ val = float(ptliststr[pos:])
+ if i % 2 == 1:
+ pairs.append((last, val))
+ else:
+ log.warning("Incomplete coordinates.")
+
+ return pairs
+
+
+def parse_svg_transform(trstr):
+ """
+ Parses an SVG transform string into a list
+ of transform names and their parameters.
+
+ Possible transformations are:
+
+ * Translate: translate( []), which specifies
+ a translation by tx and ty. If is not provided,
+ it is assumed to be zero. Result is
+ ['translate', tx, ty]
+
+ * Scale: scale( []), which specifies a scale operation
+ by sx and sy. If is not provided, it is assumed to be
+ equal to . Result is: ['scale', sx, sy]
+
+ * Rotate: rotate( [ ]), which specifies
+ a rotation by degrees about a given point.
+ If optional parameters and are not supplied,
+ the rotate is about the origin of the current user coordinate
+ system. Result is: ['rotate', rotate-angle, cx, cy]
+
+ * Skew: skewX(), which specifies a skew
+ transformation along the x-axis. skewY(), which
+ specifies a skew transformation along the y-axis.
+ Result is ['skew', angle-x, angle-y]
+
+ * Matrix: matrix( ), which specifies a
+ transformation in the form of a transformation matrix of six
+ values. matrix(a,b,c,d,e,f) is equivalent to applying the
+ transformation matrix [a b c d e f]. Result is
+ ['matrix', a, b, c, d, e, f]
+
+ Note: All parameters to the transformations are "numbers",
+ i.e. no units present.
+
+ :param trstr: SVG transform string.
+ :type trstr: str
+ :return: List of transforms.
+ :rtype: list
+ """
+ trlist = []
+
+ assert isinstance(trstr, str)
+ trstr = trstr.strip(' ')
+
+ integer_re_str = r'[+-]?[0-9]+'
+ number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
+ r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
+
+ # num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
+ comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
+ translate_re_str = r'translate\s*\(\s*(' + \
+ number_re_str + r')(?:' + \
+ comma_or_space_re_str + \
+ r'(' + number_re_str + r'))?\s*\)'
+ scale_re_str = r'scale\s*\(\s*(' + \
+ number_re_str + r')' + \
+ r'(?:' + comma_or_space_re_str + \
+ r'(' + number_re_str + r'))?\s*\)'
+ skew_re_str = r'skew([XY])\s*\(\s*(' + \
+ number_re_str + r')\s*\)'
+ rotate_re_str = r'rotate\s*\(\s*(' + \
+ number_re_str + r')' + \
+ r'(?:' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')' + \
+ comma_or_space_re_str + \
+ r'(' + number_re_str + r'))?\s*\)'
+ matrix_re_str = r'matrix\s*\(\s*' + \
+ r'(' + number_re_str + r')' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')' + comma_or_space_re_str + \
+ r'(' + number_re_str + r')\s*\)'
+
+ while len(trstr) > 0:
+ match = re.search(r'^' + translate_re_str, trstr)
+ if match:
+ trlist.append([
+ 'translate',
+ float(match.group(1)),
+ float(match.group(2)) if match.group else 0.0
+ ])
+ trstr = trstr[len(match.group(0)):].strip(' ')
+ continue
+
+ match = re.search(r'^' + scale_re_str, trstr)
+ if match:
+ trlist.append([
+ 'translate',
+ float(match.group(1)),
+ float(match.group(2)) if match.group else float(match.group(1))
+ ])
+ trstr = trstr[len(match.group(0)):].strip(' ')
+ continue
+
+ match = re.search(r'^' + skew_re_str, trstr)
+ if match:
+ trlist.append([
+ 'skew',
+ float(match.group(2)) if match.group(1) == 'X' else 0.0,
+ float(match.group(2)) if match.group(1) == 'Y' else 0.0
+ ])
+ trstr = trstr[len(match.group(0)):].strip(' ')
+ continue
+
+ match = re.search(r'^' + rotate_re_str, trstr)
+ if match:
+ trlist.append([
+ 'rotate',
+ float(match.group(1)),
+ float(match.group(2)) if match.group(2) else 0.0,
+ float(match.group(3)) if match.group(3) else 0.0
+ ])
+ trstr = trstr[len(match.group(0)):].strip(' ')
+ continue
+
+ match = re.search(r'^' + matrix_re_str, trstr)
+ if match:
+ trlist.append(['matrix'] + [float(x) for x in match.groups()])
+ trstr = trstr[len(match.group(0)):].strip(' ')
+ continue
+
+ raise Exception("Don't know how to parse: %s" % trstr)
+
+ return trlist
+
+
+if __name__ == "__main__":
+ tree = ET.parse('tests/svg/drawing.svg')
+ root = tree.getroot()
+ ns = re.search(r'\{(.*)\}', root.tag).group(1)
+ print ns
+ for geo in getsvggeo(root):
+ print geo
\ No newline at end of file
diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py
new file mode 100644
index 0000000..470358f
--- /dev/null
+++ b/tclCommands/TclCommand.py
@@ -0,0 +1,398 @@
+import sys
+import re
+import FlatCAMApp
+import abc
+import collections
+from PyQt4 import QtCore
+from contextlib import contextmanager
+
+
+class TclCommand(object):
+
+ # FlatCAMApp
+ app = None
+
+ # logger
+ log = None
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = []
+
+ # dictionary of types from Tcl command, needs to be ordered
+ # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+ option_types = collections.OrderedDict()
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
+ help = {
+ 'main': "undefined help.",
+ 'args': collections.OrderedDict([
+ ('argumentname', 'undefined help.'),
+ ('optionname', 'undefined help.')
+ ]),
+ 'examples': []
+ }
+
+ # original incoming arguments into command
+ original_args = None
+
+ def __init__(self, app):
+ self.app = app
+ if self.app is None:
+ raise TypeError('Expected app to be FlatCAMApp instance.')
+ if not isinstance(self.app, FlatCAMApp.App):
+ raise TypeError('Expected FlatCAMApp, got %s.' % type(app))
+ self.log = self.app.log
+
+ def raise_tcl_error(self, text):
+ """
+ this method pass exception from python into TCL as error, so we get stacktrace and reason
+ this is only redirect to self.app.raise_tcl_error
+ :param text: text of error
+ :return: none
+ """
+
+ self.app.raise_tcl_error(text)
+
+ def get_current_command(self):
+ """
+ get current command, we are not able to get it from TCL we have to reconstruct it
+ :return: current command
+ """
+ command_string = []
+ command_string.append(self.aliases[0])
+ if self.original_args is not None:
+ for arg in self.original_args:
+ command_string.append(arg)
+ return " ".join(command_string)
+
+ def get_decorated_help(self):
+ """
+ Decorate help for TCL console output.
+
+ :return: decorated help from structure
+ """
+
+ def get_decorated_command(alias_name):
+ command_string = []
+ for arg_key, arg_type in self.help['args'].items():
+ command_string.append(get_decorated_argument(arg_key, arg_type, True))
+ return "> " + alias_name + " " + " ".join(command_string)
+
+ def get_decorated_argument(help_key, help_text, in_command=False):
+ option_symbol = ''
+ if help_key in self.arg_names:
+ arg_type = self.arg_names[help_key]
+ type_name = str(arg_type.__name__)
+ in_command_name = "<" + type_name + ">"
+ elif help_key in self.option_types:
+ option_symbol = '-'
+ arg_type = self.option_types[help_key]
+ type_name = str(arg_type.__name__)
+ in_command_name = option_symbol + help_key + " <" + type_name + ">"
+ else:
+ option_symbol = ''
+ type_name = '?'
+ in_command_name = option_symbol + help_key + " <" + type_name + ">"
+
+ if in_command:
+ if help_key in self.required:
+ return in_command_name
+ else:
+ return '[' + in_command_name + "]"
+ else:
+ if help_key in self.required:
+ return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text
+ else:
+ return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]"
+
+ def get_decorated_example(example_item):
+ return "> "+example_item
+
+ help_string = [self.help['main']]
+ for alias in self.aliases:
+ help_string.append(get_decorated_command(alias))
+
+ for key, value in self.help['args'].items():
+ help_string.append(get_decorated_argument(key, value))
+
+ # timeout is unique for signaled commands (this is not best oop practice, but much easier for now)
+ if isinstance(self, TclCommandSignaled):
+ help_string.append("\t[-timeout : Max wait for job timeout before error.]")
+
+ for example in self.help['examples']:
+ help_string.append(get_decorated_example(example))
+
+ return "\n".join(help_string)
+
+ @staticmethod
+ def parse_arguments(args):
+ """
+ Pre-processes arguments to detect '-keyword value' pairs into dictionary
+ and standalone parameters into list.
+
+ This is copy from FlatCAMApp.setup_shell().h() just for accessibility,
+ original should be removed after all commands will be converted
+
+ :param args: arguments from tcl to parse
+ :return: arguments, options
+ """
+
+ options = {}
+ arguments = []
+ n = len(args)
+ name = None
+ for i in range(n):
+ match = re.search(r'^-([a-zA-Z].*)', args[i])
+ if match:
+ assert name is None
+ name = match.group(1)
+ continue
+
+ if name is None:
+ arguments.append(args[i])
+ else:
+ options[name] = args[i]
+ name = None
+
+ return arguments, options
+
+ def check_args(self, args):
+ """
+ Check arguments and options for right types
+
+ :param args: arguments from tcl to check
+ :return: named_args, unnamed_args
+ """
+
+ arguments, options = self.parse_arguments(args)
+
+ named_args = {}
+ unnamed_args = []
+
+ # check arguments
+ idx = 0
+ arg_names_items = self.arg_names.items()
+ for argument in arguments:
+ if len(self.arg_names) > idx:
+ key, arg_type = arg_names_items[idx]
+ try:
+ named_args[key] = arg_type(argument)
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'."
+ % (key, arg_type, str(e)))
+ else:
+ unnamed_args.append(argument)
+ idx += 1
+
+ # check options
+ for key in options:
+ if key not in self.option_types and key != 'timeout':
+ self.raise_tcl_error('Unknown parameter: %s' % key)
+ try:
+ if key != 'timeout':
+ named_args[key] = self.option_types[key](options[key])
+ else:
+ named_args[key] = int(options[key])
+ except Exception, e:
+ self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
+ % (key, self.option_types[key], str(e)))
+
+ # check required arguments
+ for key in self.required:
+ if key not in named_args:
+ self.raise_tcl_error("Missing required argument '%s'." % key)
+
+ return named_args, unnamed_args
+
+
+ def raise_tcl_unknown_error(self, unknownException):
+ """
+ raise Exception if is different type than TclErrorException
+ this is here mainly to show unknown errors inside TCL shell console
+ :param unknownException:
+ :return:
+ """
+
+ #if not isinstance(unknownException, self.TclErrorException):
+ # self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+ #else:
+ raise unknownException
+
+ def raise_tcl_error(self, text):
+ """
+ this method pass exception from python into TCL as error, so we get stacktrace and reason
+ :param text: text of error
+ :return: raise exception
+ """
+
+ # becouse of signaling we cannot call error to TCL from here but when task is finished
+ # also nonsiglaned arwe handled here to better exception handling and diplay after command is finished
+ raise self.app.TclErrorException(text)
+
+ def execute_wrapper(self, *args):
+ """
+ Command which is called by tcl console when current commands aliases are hit.
+ Main catch(except) is implemented here.
+ This method should be reimplemented only when initial checking sequence differs
+
+ :param args: arguments passed from tcl command console
+ :return: None, output text or exception
+ """
+
+ #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
+
+ try:
+ self.log.debug("TCL command '%s' executed." % str(self.__class__))
+ self.original_args=args
+ args, unnamed_args = self.check_args(args)
+ return self.execute(args, unnamed_args)
+ except Exception as unknown:
+ error_info=sys.exc_info()
+ self.log.error("TCL command '%s' failed." % str(self))
+ self.app.display_tcl_error(unknown, error_info)
+ self.raise_tcl_unknown_error(unknown)
+
+ @abc.abstractmethod
+ def execute(self, args, unnamed_args):
+ """
+ Direct execute of command, this method should be implemented in each descendant.
+ No main catch should be implemented here.
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None, output text or exception
+ """
+
+ raise NotImplementedError("Please Implement this method")
+
+class TclCommandSignaled(TclCommand):
+ """
+ !!! I left it here only for demonstration !!!
+ Go to TclCommandCncjob and into class definition put
+ class TclCommandCncjob(TclCommand.TclCommandSignaled):
+ also change
+ obj.generatecncjob(use_thread = False, **args)
+ to
+ obj.generatecncjob(use_thread = True, **args)
+
+
+ This class is child of TclCommand and is used for commands which create new objects
+ it handles all neccessary stuff about blocking and passing exeptions
+ """
+
+ output = None
+
+ def execute_call(self, args, unnamed_args):
+
+ try:
+ self.output = None
+ self.error=None
+ self.error_info=None
+ self.output = self.execute(args, unnamed_args)
+ except Exception as unknown:
+ self.error_info = sys.exc_info()
+ self.error=unknown
+ finally:
+ self.app.shell_command_finished.emit(self)
+
+ def execute_wrapper(self, *args):
+ """
+ Command which is called by tcl console when current commands aliases are hit.
+ Main catch(except) is implemented here.
+ This method should be reimplemented only when initial checking sequence differs
+
+ :param args: arguments passed from tcl command console
+ :return: None, output text or exception
+ """
+
+ @contextmanager
+ def wait_signal(signal, timeout=300000):
+ """Block loop until signal emitted, or timeout (ms) elapses."""
+ loop = QtCore.QEventLoop()
+
+ # Normal termination
+ signal.connect(loop.quit)
+
+ # Termination by exception in thread
+ self.app.thread_exception.connect(loop.quit)
+
+ status = {'timed_out': False}
+
+ def report_quit():
+ status['timed_out'] = True
+ loop.quit()
+
+ yield
+
+ # Temporarily change how exceptions are managed.
+ oeh = sys.excepthook
+ ex = []
+
+ def except_hook(type_, value, traceback_):
+ ex.append(value)
+ oeh(type_, value, traceback_)
+ sys.excepthook = except_hook
+
+ # Terminate on timeout
+ if timeout is not None:
+ QtCore.QTimer.singleShot(timeout, report_quit)
+
+ # Block
+ loop.exec_()
+
+ # Restore exception management
+ sys.excepthook = oeh
+ if ex:
+ raise ex[0]
+
+ if status['timed_out']:
+ self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout ' for command or 'set_sys background_timeout '.")
+
+ try:
+ self.log.debug("TCL command '%s' executed." % str(self.__class__))
+ self.original_args=args
+ args, unnamed_args = self.check_args(args)
+ if 'timeout' in args:
+ passed_timeout=args['timeout']
+ del args['timeout']
+ else:
+ passed_timeout= self.app.defaults['background_timeout']
+
+ # set detail for processing, it will be there until next open or close
+ self.app.shell.open_proccessing(self.get_current_command())
+
+ def handle_finished(obj):
+ self.app.shell_command_finished.disconnect(handle_finished)
+ if self.error is not None:
+ self.raise_tcl_unknown_error(self.error)
+
+ self.app.shell_command_finished.connect(handle_finished)
+
+ with wait_signal(self.app.shell_command_finished, passed_timeout):
+ # every TclCommandNewObject ancestor support timeout as parameter,
+ # but it does not mean anything for child itself
+ # when operation will be really long is good to set it higher then defqault 30s
+ self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
+
+ return self.output
+
+ except Exception as unknown:
+ # if error happens inside thread execution, then pass correct error_info to display
+ if self.error_info is not None:
+ error_info = self.error_info
+ else:
+ error_info=sys.exc_info()
+ self.log.error("TCL command '%s' failed." % str(self))
+ self.app.display_tcl_error(unknown, error_info)
+ self.raise_tcl_unknown_error(unknown)
\ No newline at end of file
diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py
new file mode 100644
index 0000000..c9e3507
--- /dev/null
+++ b/tclCommands/TclCommandAddPolygon.py
@@ -0,0 +1,61 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandAddPolygon(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to create a polygon in the given Geometry object
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['add_polygon', 'add_poly']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict()
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Creates a polygon in the given Geometry object.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the Geometry object to which to append the polygon.'),
+ ('xi, yi', 'Coordinates of points in the polygon.')
+ ]),
+ 'examples': [
+ 'add_polygon [x3 y3 [...]]'
+ ]
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ if len(unnamed_args) % 2 != 0:
+ self.raise_tcl_error("Incomplete coordinates.")
+
+ points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+
+ obj.add_polygon(points)
+ obj.plot()
diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py
new file mode 100644
index 0000000..3c99476
--- /dev/null
+++ b/tclCommands/TclCommandAddPolyline.py
@@ -0,0 +1,61 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandAddPolyline(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to create a polyline in the given Geometry object
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['add_polyline']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict()
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Creates a polyline in the given Geometry object.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the Geometry object to which to append the polyline.'),
+ ('xi, yi', 'Coordinates of points in the polyline.')
+ ]),
+ 'examples': [
+ 'add_polyline [x3 y3 [...]]'
+ ]
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ if len(unnamed_args) % 2 != 0:
+ self.raise_tcl_error("Incomplete coordinates.")
+
+ points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+
+ obj.add_polyline(points)
+ obj.plot()
diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py
new file mode 100644
index 0000000..e6d84de
--- /dev/null
+++ b/tclCommands/TclCommandCncjob.py
@@ -0,0 +1,80 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandCncjob(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to Generates a CNC Job from a Geometry Object.
+
+ example:
+ set_sys units MM
+ new
+ open_gerber tests/gerber_files/simple1.gbr -outname margin
+ isolate margin -dia 3
+ cncjob margin_iso
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['cncjob']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict([
+ ('z_cut',float),
+ ('z_move',float),
+ ('feedrate',float),
+ ('tooldia',float),
+ ('spindlespeed',int),
+ ('multidepth',bool),
+ ('depthperpass',float),
+ ('outname',str)
+ ])
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Generates a CNC Job from a Geometry Object.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the source object.'),
+ ('z_cut', 'Z-axis cutting position.'),
+ ('z_move', 'Z-axis moving position.'),
+ ('feedrate', 'Moving speed when cutting.'),
+ ('tooldia', 'Tool diameter to show on screen.'),
+ ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
+ ('multidepth', 'Use or not multidepth cnccut.'),
+ ('depthperpass', 'Height of one layer for multidepth.'),
+ ('outname', 'Name of the resulting Geometry object.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ if 'outname' not in args:
+ args['outname'] = name + "_cnc"
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, FlatCAMGeometry):
+ self.raise_tcl_error('Expected FlatCAMGeometry, got %s %s.' % (name, type(obj)))
+
+ del args['name']
+ obj.generatecncjob(use_thread = False, **args)
\ No newline at end of file
diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py
new file mode 100644
index 0000000..783b659
--- /dev/null
+++ b/tclCommands/TclCommandDrillcncjob.py
@@ -0,0 +1,81 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandDrillcncjob(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to Generates a Drill CNC Job from a Excellon Object.
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['drillcncjob']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict([
+ ('tools',str),
+ ('drillz',float),
+ ('travelz',float),
+ ('feedrate',float),
+ ('spindlespeed',int),
+ ('toolchange',bool),
+ ('outname',str)
+ ])
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Generates a Drill CNC Job from a Excellon Object.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the source object.'),
+ ('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'),
+ ('drillz', 'Drill depth into material (example: -2.0).'),
+ ('travelz', 'Travel distance above material (example: 2.0).'),
+ ('feedrate', 'Drilling feed rate.'),
+ ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
+ ('toolchange', 'Enable tool changes (example: True).'),
+ ('outname', 'Name of the resulting Geometry object.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ if 'outname' not in args:
+ args['outname'] = name + "_cnc"
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, FlatCAMExcellon):
+ self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj)))
+
+ def job_init(job_obj, app):
+ job_obj.z_cut = args["drillz"]
+ job_obj.z_move = args["travelz"]
+ job_obj.feedrate = args["feedrate"]
+ job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
+ toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
+ tools = args["tools"] if "tools" in args else 'all'
+ job_obj.generate_from_excellon_by_tool(obj, tools, toolchange)
+ job_obj.gcode_parse()
+ job_obj.create_geometry()
+
+ self.app.new_object("cncjob", args['outname'], job_init)
diff --git a/tclCommands/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py
new file mode 100644
index 0000000..feecd87
--- /dev/null
+++ b/tclCommands/TclCommandExportGcode.py
@@ -0,0 +1,79 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandExportGcode(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to export gcode as tcl output for "set X [export_gcode ...]"
+
+ Requires name to be available. It might still be in the
+ making at the time this function is called, so check for
+ promises and send to background if there are promises.
+
+
+ this export may be catched by tcl and past as preable to another export_gcode or write_gcode
+ this can be used to join GCODES
+
+ example:
+ set_sys units MM
+ new
+ open_gerber tests/gerber_files/simple1.gbr -outname margin
+ isolate margin -dia 3
+ cncjob margin_iso
+ cncjob margin_iso
+ set EXPORT [export_gcode margin_iso_cnc]
+ write_gcode margin_iso_cnc_1 /tmp/file.gcode ${EXPORT}
+
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['export_gcode']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str),
+ ('preamble', str),
+ ('postamble', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict()
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Export gcode into console output.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the source Geometry object.'),
+ ('preamble', 'Prepend GCODE.'),
+ ('postamble', 'Append GCODE.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, CNCjob):
+ self.raise_tcl_error('Expected CNCjob, got %s %s.' % (name, type(obj)))
+
+ if self.app.collection.has_promises():
+ self.raise_tcl_error('!!!Promises exists, but should not here!!!')
+
+ del args['name']
+ return obj.get_gcode(**args)
diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py
new file mode 100644
index 0000000..ac69e7c
--- /dev/null
+++ b/tclCommands/TclCommandExteriors.py
@@ -0,0 +1,64 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandExteriors(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to get exteriors of polygons
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['exteriors', 'ext']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict([
+ ('outname', str)
+ ])
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Get exteriors of polygons.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the source Geometry object.'),
+ ('outname', 'Name of the resulting Geometry object.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ if 'outname' in args:
+ outname = args['outname']
+ else:
+ outname = name + "_exteriors"
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ def geo_init(geo_obj, app_obj):
+ geo_obj.solid_geometry = obj_exteriors
+
+ obj_exteriors = obj.get_exteriors()
+ self.app.new_object('geometry', outname, geo_init)
diff --git a/tclCommands/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py
new file mode 100644
index 0000000..51cc190
--- /dev/null
+++ b/tclCommands/TclCommandImportSvg.py
@@ -0,0 +1,81 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandImportSvg(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to import an SVG file as a Geometry Object.
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['import_svg']
+
+ # 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([
+ ('type', 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': "Import an SVG file as a Geometry Object..",
+ 'args': collections.OrderedDict([
+ ('filename', 'Path to file to open.'),
+ ('type', 'Import as gerber or geometry(default).'),
+ ('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(geo_obj, app_obj):
+
+ if not isinstance(geo_obj, Geometry):
+ self.raise_tcl_error('Expected Geometry or Gerber, got %s %s.' % (outname, type(geo_obj)))
+
+ geo_obj.import_svg(filename)
+
+ filename = args['filename']
+
+ if 'outname' in args:
+ outname = args['outname']
+ else:
+ outname = filename.split('/')[-1].split('\\')[-1]
+
+ if 'type' in args:
+ obj_type = args['type']
+ else:
+ obj_type = 'geometry'
+
+ if obj_type != "geometry" and obj_type != "gerber":
+ self.raise_tcl_error("Option type can be 'geopmetry' or 'gerber' only, got '%s'." % obj_type)
+
+ with self.app.proc_container.new("Import SVG"):
+
+ # Object creation
+ self.app.new_object(obj_type, outname, obj_init)
+
+ # Register recent file
+ self.app.file_opened.emit("svg", filename)
+
+ # GUI feedback
+ self.app.inform.emit("Opened: " + filename)
+
diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py
new file mode 100644
index 0000000..61bfe9f
--- /dev/null
+++ b/tclCommands/TclCommandInteriors.py
@@ -0,0 +1,64 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandInteriors(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to get interiors of polygons
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['interiors']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('name', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict([
+ ('outname', str)
+ ])
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['name']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Get interiors of polygons.",
+ 'args': collections.OrderedDict([
+ ('name', 'Name of the source Geometry object.'),
+ ('outname', 'Name of the resulting Geometry object.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ name = args['name']
+
+ if 'outname' in args:
+ outname = args['outname']
+ else:
+ outname = name + "_interiors"
+
+ obj = self.app.collection.get_by_name(name)
+ if obj is None:
+ self.raise_tcl_error("Object not found: %s" % name)
+
+ if not isinstance(obj, Geometry):
+ self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+
+ def geo_init(geo_obj, app_obj):
+ geo_obj.solid_geometry = obj_exteriors
+
+ obj_exteriors = obj.get_interiors()
+ self.app.new_object('geometry', outname, geo_init)
diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py
new file mode 100644
index 0000000..8c51f21
--- /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 0000000..db3fe57
--- /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 0000000..a951d8f
--- /dev/null
+++ b/tclCommands/TclCommandOpenGerber.py
@@ -0,0 +1,95 @@
+from ObjectCollection import *
+import TclCommand
+
+
+class TclCommandOpenGerber(TclCommand.TclCommandSignaled):
+ """
+ Tcl shell command to opens a Gerber file
+ """
+
+ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+ aliases = ['open_gerber']
+
+ # dictionary of types from Tcl command, needs to be ordered
+ arg_names = collections.OrderedDict([
+ ('filename', str)
+ ])
+
+ # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
+ option_types = collections.OrderedDict([
+ ('follow', str),
+ ('outname', str)
+ ])
+
+ # array of mandatory options for current Tcl command: required = {'name','outname'}
+ required = ['filename']
+
+ # structured help for current command, args needs to be ordered
+ help = {
+ 'main': "Opens a Gerber file.",
+ 'args': collections.OrderedDict([
+ ('filename', 'Path to file to open.'),
+ ('follow', 'N If 1, does not create polygons, just follows the gerber path.'),
+ ('outname', 'Name of the resulting Geometry object.')
+ ]),
+ 'examples': []
+ }
+
+ def execute(self, args, unnamed_args):
+ """
+ execute current TCL shell command
+
+ :param args: array of known named arguments and options
+ :param unnamed_args: array of other values which were passed into command
+ without -somename and we do not have them in known arg_names
+ :return: None or exception
+ """
+
+ # How the object should be initialized
+ def obj_init(gerber_obj, app_obj):
+
+ if not isinstance(gerber_obj, Geometry):
+ self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (outname, type(gerber_obj)))
+
+ # Opening the file happens here
+ self.app.progress.emit(30)
+ try:
+ gerber_obj.parse_file(filename, follow=follow)
+
+ except IOError:
+ app_obj.inform.emit("[error] Failed to open file: %s " % filename)
+ app_obj.progress.emit(0)
+ self.raise_tcl_error('Failed to open file: %s' % filename)
+
+ except ParseError, e:
+ app_obj.inform.emit("[error] Failed to parse file: %s, %s " % (filename, str(e)))
+ app_obj.progress.emit(0)
+ self.log.error(str(e))
+ raise
+
+ # Further parsing
+ app_obj.progress.emit(70)
+
+ filename = args['filename']
+
+ if 'outname' in args:
+ outname = args['outname']
+ else:
+ outname = filename.split('/')[-1].split('\\')[-1]
+
+ follow = None
+ if 'follow' in args:
+ follow = args['follow']
+
+ with self.app.proc_container.new("Opening Gerber"):
+
+ # Object creation
+ self.app.new_object("gerber", outname, obj_init)
+
+ # Register recent file
+ self.app.file_opened.emit("gerber", filename)
+
+ self.app.progress.emit(100)
+
+ # GUI feedback
+ self.app.inform.emit("Opened: " + filename)
diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py
new file mode 100644
index 0000000..47e65b4
--- /dev/null
+++ b/tclCommands/__init__.py
@@ -0,0 +1,54 @@
+import pkgutil
+import sys
+
+# allowed command modules (please append them alphabetically ordered)
+import tclCommands.TclCommandAddPolygon
+import tclCommands.TclCommandAddPolyline
+import tclCommands.TclCommandCncjob
+import tclCommands.TclCommandDrillcncjob
+import tclCommands.TclCommandExportGcode
+import tclCommands.TclCommandExteriors
+import tclCommands.TclCommandImportSvg
+import tclCommands.TclCommandInteriors
+import tclCommands.TclCommandIsolate
+import tclCommands.TclCommandNew
+import tclCommands.TclCommandOpenGerber
+
+
+__all__ = []
+
+for loader, name, is_pkg in pkgutil.walk_packages(__path__):
+ module = loader.find_module(name).load_module(name)
+ __all__.append(name)
+
+
+def register_all_commands(app, commands):
+ """
+ Static method which register all known commands.
+
+ Command should be for now in directory tclCommands and module should start with TCLCommand
+ Class have to follow same name as module.
+
+ we need import all modules in top section:
+ import tclCommands.TclCommandExteriors
+ at this stage we can include only wanted commands with this, auto loading may be implemented in future
+ I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc.
+
+ :param app: FlatCAMApp
+ :param commands: array of commands which should be modified
+ :return: None
+ """
+
+ tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')}
+
+ for key, mod in tcl_modules.items():
+ if key != 'tclCommands.TclCommand':
+ class_name = key.split('.')[1]
+ class_type = getattr(mod, class_name)
+ command_instance = class_type(app)
+
+ for alias in command_instance.aliases:
+ commands[alias] = {
+ 'fcn': command_instance.execute_wrapper,
+ 'help': command_instance.get_decorated_help()
+ }
diff --git a/termwidget.py b/termwidget.py
index d6309fd..538cc16 100644
--- a/termwidget.py
+++ b/termwidget.py
@@ -4,8 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history.
"""
import cgi
-
-from PyQt4.QtCore import pyqtSignal
+from PyQt4.QtCore import pyqtSignal, Qt
from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
QSizePolicy, QTextCursor, QTextEdit, \
QVBoxLayout, QWidget
@@ -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)
@@ -113,6 +116,34 @@ class TermWidget(QWidget):
self._edit.setFocus()
+ def open_proccessing(self, detail=None):
+ """
+ Open processing and disable using shell commands again until all commands are finished
+
+ :param detail: text detail about what is currently called from TCL to python
+ :return: None
+ """
+
+ self._edit.setTextColor(Qt.white)
+ self._edit.setTextBackgroundColor(Qt.darkGreen)
+ if detail is None:
+ self._edit.setPlainText("...proccessing...")
+ else:
+ self._edit.setPlainText("...proccessing... [%s]" % detail)
+
+ self._edit.setDisabled(True)
+
+ def close_proccessing(self):
+ """
+ Close processing and enable using shell commands again
+ :return:
+ """
+
+ self._edit.setTextColor(Qt.black)
+ self._edit.setTextBackgroundColor(Qt.white)
+ self._edit.setPlainText('')
+ self._edit.setDisabled(False)
+
def _append_to_browser(self, style, text):
"""
Convert text to HTML for inserting it to browser
@@ -120,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!!!
@@ -238,4 +251,3 @@ class TermWidget(QWidget):
self._historyIndex -= 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
-
diff --git a/tests/canvas/performance.py b/tests/canvas/performance.py
new file mode 100644
index 0000000..9478bcb
--- /dev/null
+++ b/tests/canvas/performance.py
@@ -0,0 +1,95 @@
+from __future__ import division
+import matplotlib
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt
+import numpy as np
+import cStringIO
+from matplotlib.backends.backend_agg import FigureCanvasAgg
+from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
+from matplotlib.figure import Figure
+import cProfile
+import sys
+
+
+def gen_data():
+ N = 100000
+ x = np.random.rand(N) * 10
+ y = np.random.rand(N) * 10
+ colors = np.random.rand(N)
+ area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radiuses
+ data = x, y, area, colors
+ return data
+
+
+# @profile
+def large_plot(data):
+ x, y, area, colors = data
+
+ fig = Figure(figsize=(10, 10), dpi=80)
+ axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
+ axes.set_frame_on(False)
+ axes.set_xticks([])
+ axes.set_yticks([])
+ # axes.set_xlim(0, 10)
+ # axes.set_ylim(0, 10)
+
+ axes.scatter(x, y, s=area, c=colors, alpha=0.5)
+
+ axes.set_xlim(0, 10)
+ axes.set_ylim(0, 10)
+
+ canvas = FigureCanvasAgg(fig)
+ canvas.draw()
+ # canvas = FigureCanvasQTAgg(fig)
+ # buf = canvas.tostring_rgb()
+ buf = fig.canvas.tostring_rgb()
+
+ ncols, nrows = fig.canvas.get_width_height()
+ img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
+
+ return img
+
+
+def small_plot(data):
+ x, y, area, colors = data
+
+ fig = Figure(figsize=(3, 3), dpi=80)
+ axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
+ axes.set_frame_on(False)
+ axes.set_xticks([])
+ axes.set_yticks([])
+ # axes.set_xlim(5, 6)
+ # axes.set_ylim(5, 6)
+
+ axes.scatter(x, y, s=area, c=colors, alpha=0.5)
+
+ axes.set_xlim(4, 7)
+ axes.set_ylim(4, 7)
+
+ canvas = FigureCanvasAgg(fig)
+ canvas.draw()
+ # canvas = FigureCanvasQTAgg(fig)
+ # buf = canvas.tostring_rgb()
+ buf = fig.canvas.tostring_rgb()
+
+ ncols, nrows = fig.canvas.get_width_height()
+ img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
+
+ return img
+
+def doit():
+ d = gen_data()
+ img = large_plot(d)
+ return img
+
+
+if __name__ == "__main__":
+
+ d = gen_data()
+
+ if sys.argv[1] == 'large':
+ cProfile.runctx('large_plot(d)', None, locals())
+ else:
+ cProfile.runctx('small_plot(d)', None, locals())
+
+
diff --git a/tests/canvas/prof.sh b/tests/canvas/prof.sh
new file mode 100755
index 0000000..b907584
--- /dev/null
+++ b/tests/canvas/prof.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo "*** LARGE ***"
+python performance.py large | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)"
+echo "*** SMALL ***"
+python performance.py small | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)"
\ No newline at end of file
diff --git a/tests/excellon_files/case1.drl b/tests/excellon_files/case1.drl
new file mode 100644
index 0000000..95b89ca
--- /dev/null
+++ b/tests/excellon_files/case1.drl
@@ -0,0 +1,125 @@
+M48
+INCH
+T01C0.0200
+T02C0.0800
+T03C0.0600
+T04C0.0300
+T05C0.0650
+T06C0.0450
+T07C0.0400
+T08C0.1181
+T09C0.0500
+%
+T01
+X-018204Y+015551
+X-025842Y+015551
+T02
+X-000118Y+020629
+X-000118Y+016889
+X+012401Y+020629
+X+012401Y+016889
+X-010170Y+002440
+X-010110Y+011470
+X+018503Y+026574
+T03
+X+013060Y+010438
+X+013110Y+000000
+X-049015Y+002165
+X+018378Y+010433
+X+018317Y+000000
+X-049015Y+010039
+X-041141Y-000629
+X-041181Y+012992
+X-056496Y+012992
+X-056496Y-000590
+T04
+X-037560Y+030490
+X-036560Y+030490
+X-035560Y+030490
+X-034560Y+030490
+X-033560Y+030490
+X-032560Y+030490
+X-031560Y+030490
+X-030560Y+030490
+X-029560Y+030490
+X-028560Y+030490
+X-027560Y+030490
+X-026560Y+030490
+X-025560Y+030490
+X-024560Y+030490
+X-024560Y+036490
+X-025560Y+036490
+X-026560Y+036490
+X-027560Y+036490
+X-028560Y+036490
+X-029560Y+036490
+X-030560Y+036490
+X-031560Y+036490
+X-032560Y+036490
+X-033560Y+036490
+X-034560Y+036490
+X-035560Y+036490
+X-036560Y+036490
+X-037560Y+036490
+X-014590Y+030810
+X-013590Y+030810
+X-012590Y+030810
+X-011590Y+030810
+X-011590Y+033810
+X-012590Y+033810
+X-013590Y+033810
+X-014590Y+033810
+X-021260Y+034680
+X-020010Y+034680
+X-008390Y+035840
+X-008390Y+034590
+X-008440Y+031870
+X-008440Y+030620
+T05
+X-022504Y+019291
+X-020354Y+019291
+X-018204Y+019291
+X-030142Y+019291
+X-027992Y+019291
+X-025842Y+019291
+X-012779Y+019291
+X-010629Y+019291
+X-008479Y+019291
+T06
+X-028080Y+028230
+X-030080Y+028230
+X-034616Y+024409
+X-039616Y+024409
+X-045364Y+023346
+X-045364Y+018346
+X-045364Y+030157
+X-045364Y+025157
+X-008604Y+026983
+X-013604Y+026983
+X-016844Y+034107
+X-016844Y+029107
+T07
+X-041655Y+026456
+X-040655Y+026456
+X-039655Y+026456
+X-041640Y+022047
+X-040640Y+022047
+X-039640Y+022047
+X-049760Y+029430
+X-048760Y+029430
+X-047760Y+029430
+X-019220Y+037380
+X-020220Y+037380
+X-021220Y+037380
+T08
+X-024212Y+007751
+X-024212Y+004011
+X-035629Y+007874
+X-035629Y+004133
+T09
+X+007086Y+030708
+X+007086Y+032874
+X-000787Y+031889
+X-000787Y+035826
+X-000787Y+027952
+M30
diff --git a/tests/gerber_files/detector_contour.gbr b/tests/gerber_files/detector_contour.gbr
new file mode 100644
index 0000000..93adef0
--- /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 0000000..d3bca48
--- /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 0000000..52b2e2a
--- /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 0000000..c4945b8
--- /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/svg/7segment_9,9.svg b/tests/svg/7segment_9,9.svg
new file mode 100644
index 0000000..ffe7c65
--- /dev/null
+++ b/tests/svg/7segment_9,9.svg
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/tests/svg/Arduino Nano3_pcb.svg b/tests/svg/Arduino Nano3_pcb.svg
new file mode 100644
index 0000000..f1f3b0c
--- /dev/null
+++ b/tests/svg/Arduino Nano3_pcb.svg
@@ -0,0 +1,468 @@
+
+
+
+
diff --git a/tests/svg/drawing.svg b/tests/svg/drawing.svg
new file mode 100644
index 0000000..7feb03a
--- /dev/null
+++ b/tests/svg/drawing.svg
@@ -0,0 +1,126 @@
+
+
+
+
diff --git a/tests/svg/usb_connector.svg b/tests/svg/usb_connector.svg
new file mode 100644
index 0000000..25db707
--- /dev/null
+++ b/tests/svg/usb_connector.svg
@@ -0,0 +1,77 @@
+
\ No newline at end of file
diff --git a/tests/test_excellon_flow.py b/tests/test_excellon_flow.py
new file mode 100644
index 0000000..234dff9
--- /dev/null
+++ b/tests/test_excellon_flow.py
@@ -0,0 +1,163 @@
+import unittest
+from PyQt4 import QtGui
+import sys
+from FlatCAMApp import App
+from FlatCAMObj import FlatCAMExcellon, FlatCAMCNCjob
+from ObjectUI import ExcellonObjectUI
+import tempfile
+import os
+from time import sleep
+
+
+class ExcellonFlowTestCase(unittest.TestCase):
+ """
+ This is a top-level test covering the Excellon-to-GCode
+ generation workflow.
+
+ THIS IS A REQUIRED TEST FOR ANY UPDATES.
+
+ """
+
+ filename = 'case1.drl'
+
+ 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)
+
+ self.fc.open_excellon('tests/excellon_files/' + self.filename)
+
+ def tearDown(self):
+ del self.fc
+ del self.app
+
+ def test_flow(self):
+ # Names of available objects.
+ names = self.fc.collection.get_names()
+ print names
+
+ #--------------------------------------
+ # Total of 1 objects.
+ #--------------------------------------
+ self.assertEquals(len(names), 1,
+ "Expected 1 object, found %d" % len(names))
+
+ #--------------------------------------
+ # Object's name matches the file name.
+ #--------------------------------------
+ self.assertEquals(names[0], self.filename,
+ "Expected name == %s, got %s" % (self.filename, names[0]))
+
+ #---------------------------------------
+ # Get object by that name, make sure it's a FlatCAMExcellon.
+ #---------------------------------------
+ excellon_name = names[0]
+ excellon_obj = self.fc.collection.get_by_name(excellon_name)
+ self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),
+ "Expected FlatCAMExcellon, instead, %s is %s" %
+ (excellon_name, type(excellon_obj)))
+
+ #----------------------------------------
+ # Object's GUI matches Object's options
+ #----------------------------------------
+ # TODO: Open GUI with double-click on object.
+ # Opens the Object's GUI, populates it.
+ excellon_obj.build_ui()
+ for option, value in excellon_obj.options.iteritems():
+ try:
+ form_field = excellon_obj.form_fields[option]
+ except KeyError:
+ print ("**********************************************************\n"
+ "* WARNING: Option '{}' has no form field\n"
+ "**********************************************************"
+ "".format(option))
+ continue
+ self.assertEqual(value, form_field.get_value(),
+ "Option '{}' == {} but form has {}".format(
+ option, value, form_field.get_value()
+ ))
+
+ #--------------------------------------------------
+ # Changes in the GUI should be read in when
+ # running any process. Changing something here.
+ #--------------------------------------------------
+
+ form_field = excellon_obj.form_fields['feedrate']
+ value = form_field.get_value()
+ form_field.set_value(value * 1.1) # Increase by 10%
+ print "'feedrate' == {}".format(value)
+
+ #--------------------------------------------------
+ # Create GCode using all tools.
+ #--------------------------------------------------
+
+ assert isinstance(excellon_obj, FlatCAMExcellon) # Just for the IDE
+ ui = excellon_obj.ui
+ assert isinstance(ui, ExcellonObjectUI)
+ ui.tools_table.selectAll() # Select All
+ ui.generate_cnc_button.click() # Click
+
+ # Work is done in a separate thread and results are
+ # passed via events to the main event loop which is
+ # not running. Run only for pending events.
+ #
+ # I'm not sure why, but running it only once does
+ # not catch the new object. Might be a timing issue.
+ # http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details
+ for _ in range(2):
+ sleep(0.1)
+ self.app.processEvents()
+
+ #---------------------------------------------
+ # Check that GUI has been read in.
+ #---------------------------------------------
+
+ value = excellon_obj.options['feedrate']
+ form_value = form_field.get_value()
+ self.assertEqual(value, form_value,
+ "Form value for '{}' == {} was not read into options"
+ "which has {}".format('feedrate', form_value, value))
+ print "'feedrate' == {}".format(value)
+
+ #---------------------------------------------
+ # Check that only 1 object has been created.
+ #---------------------------------------------
+
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 2,
+ "Expected 2 objects, found %d" % len(names))
+
+ #-------------------------------------------------------
+ # Make sure the CNCJob Object has the correct name
+ #-------------------------------------------------------
+
+ cncjob_name = excellon_name + "_cnc"
+ self.assertTrue(cncjob_name in names,
+ "Object named %s not found." % cncjob_name)
+
+ #-------------------------------------------------------
+ # Get the object make sure it's a cncjob object
+ #-------------------------------------------------------
+
+ cncjob_obj = self.fc.collection.get_by_name(cncjob_name)
+ self.assertTrue(isinstance(cncjob_obj, FlatCAMCNCjob),
+ "Expected a FlatCAMCNCjob, got %s" % type(cncjob_obj))
+
+ #-----------------------------------------
+ # Export G-Code, check output
+ #-----------------------------------------
+ assert isinstance(cncjob_obj, FlatCAMCNCjob) # For IDE
+
+ # get system temporary file(try create it and delete)
+ with tempfile.NamedTemporaryFile(prefix='unittest.',
+ suffix="." + cncjob_name + '.gcode',
+ delete=True) as tmp_file:
+ output_filename = tmp_file.name
+
+ cncjob_obj.export_gcode(output_filename)
+ self.assertTrue(os.path.isfile(output_filename))
+ os.remove(output_filename)
+
+ print names
diff --git a/tests/test_gerber_flow.py b/tests/test_gerber_flow.py
index ac51b1c..a832150 100644
--- a/tests/test_gerber_flow.py
+++ b/tests/test_gerber_flow.py
@@ -1,14 +1,22 @@
import sys
import unittest
from PyQt4 import QtGui
-from FlatCAMApp import App
+from FlatCAMApp import App, tclCommands
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob
from ObjectUI import GerberObjectUI, GeometryObjectUI
from time import sleep
import os
+import tempfile
class GerberFlowTestCase(unittest.TestCase):
+ """
+ This is a top-level test covering the Gerber-to-GCode
+ generation workflow.
+
+ THIS IS A REQUIRED TEST FOR ANY UPDATES.
+
+ """
filename = 'simple1.gbr'
@@ -51,6 +59,36 @@ class GerberFlowTestCase(unittest.TestCase):
"Expected FlatCAMGerber, instead, %s is %s" %
(gerber_name, type(gerber_obj)))
+ #----------------------------------------
+ # Object's GUI matches Object's options
+ #----------------------------------------
+ # TODO: Open GUI with double-click on object.
+ # Opens the Object's GUI, populates it.
+ gerber_obj.build_ui()
+ for option, value in gerber_obj.options.iteritems():
+ try:
+ form_field = gerber_obj.form_fields[option]
+ except KeyError:
+ print ("**********************************************************\n"
+ "* WARNING: Option '{}' has no form field\n"
+ "**********************************************************"
+ "".format(option))
+ continue
+ self.assertEqual(value, form_field.get_value(),
+ "Option '{}' == {} but form has {}".format(
+ option, value, form_field.get_value()
+ ))
+
+ #--------------------------------------------------
+ # Changes in the GUI should be read in when
+ # running any process. Changing something here.
+ #--------------------------------------------------
+
+ form_field = gerber_obj.form_fields['isotooldia']
+ value = form_field.get_value()
+ form_field.set_value(value * 1.1) # Increase by 10%
+ print "'isotooldia' == {}".format(value)
+
#--------------------------------------------------
# Create isolation routing using default values
# and by clicking on the button.
@@ -58,11 +96,22 @@ class GerberFlowTestCase(unittest.TestCase):
# Get the object's GUI and click on "Generate Geometry" under
# "Isolation Routing"
assert isinstance(gerber_obj, FlatCAMGerber) # Just for the IDE
- gerber_obj.build_ui() # Open the object's UI.
+ # Changed: UI has been build already
+ #gerber_obj.build_ui() # Open the object's UI.
ui = gerber_obj.ui
assert isinstance(ui, GerberObjectUI)
ui.generate_iso_button.click() # Click
+ #---------------------------------------------
+ # Check that GUI has been read in.
+ #---------------------------------------------
+ value = gerber_obj.options['isotooldia']
+ form_value = form_field.get_value()
+ self.assertEqual(value, form_value,
+ "Form value for '{}' == {} was not read into options"
+ "which has {}".format('isotooldia', form_value, value))
+ print "'isotooldia' == {}".format(value)
+
#---------------------------------------------
# Check that only 1 object has been created.
#---------------------------------------------
@@ -128,9 +177,14 @@ class GerberFlowTestCase(unittest.TestCase):
# Export G-Code, check output
#-----------------------------------------
assert isinstance(cnc_obj, FlatCAMCNCjob)
- output_filename = "tests/tmp/" + cnc_name + ".gcode"
+ output_filename = ""
+ # get system temporary file(try create it and delete also)
+ with tempfile.NamedTemporaryFile(prefix='unittest.',
+ suffix="." + cnc_name + '.gcode',
+ delete=True) as tmp_file:
+ output_filename = tmp_file.name
cnc_obj.export_gcode(output_filename)
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
- print names
\ No newline at end of file
+ print names
diff --git a/tests/test_svg_flow.py b/tests/test_svg_flow.py
new file mode 100644
index 0000000..c3b4322
--- /dev/null
+++ b/tests/test_svg_flow.py
@@ -0,0 +1,129 @@
+import sys
+import unittest
+from PyQt4 import QtGui
+from FlatCAMApp import App
+from FlatCAMObj import FlatCAMGeometry, FlatCAMCNCjob
+from ObjectUI import GerberObjectUI, GeometryObjectUI
+from time import sleep
+import os
+import tempfile
+
+
+class SVGFlowTestCase(unittest.TestCase):
+
+ 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)
+
+ self.filename = 'drawing.svg'
+
+ def tearDown(self):
+ del self.fc
+ del self.app
+
+ def test_flow(self):
+
+ self.fc.import_svg('tests/svg/' + self.filename)
+
+ names = self.fc.collection.get_names()
+ print names
+
+ #--------------------------------------
+ # Total of 1 objects.
+ #--------------------------------------
+ self.assertEquals(len(names), 1,
+ "Expected 1 object, found %d" % len(names))
+
+ #--------------------------------------
+ # Object's name matches the file name.
+ #--------------------------------------
+ self.assertEquals(names[0], self.filename,
+ "Expected name == %s, got %s" % (self.filename, names[0]))
+
+ #---------------------------------------
+ # Get object by that name, make sure it's a FlatCAMGerber.
+ #---------------------------------------
+ geo_name = names[0]
+ geo_obj = self.fc.collection.get_by_name(geo_name)
+ self.assertTrue(isinstance(geo_obj, FlatCAMGeometry),
+ "Expected FlatCAMGeometry, instead, %s is %s" %
+ (geo_name, type(geo_obj)))
+
+ #----------------------------------------
+ # Object's GUI matches Object's options
+ #----------------------------------------
+ # TODO: Open GUI with double-click on object.
+ # Opens the Object's GUI, populates it.
+ geo_obj.build_ui()
+ for option, value in geo_obj.options.iteritems():
+ try:
+ form_field = geo_obj.form_fields[option]
+ except KeyError:
+ print ("**********************************************************\n"
+ "* WARNING: Option '{}' has no form field\n"
+ "**********************************************************"
+ "".format(option))
+ continue
+ self.assertEqual(value, form_field.get_value(),
+ "Option '{}' == {} but form has {}".format(
+ option, value, form_field.get_value()
+ ))
+
+ #------------------------------------
+ # Open the UI, make CNCObject
+ #------------------------------------
+ geo_obj.build_ui()
+ ui = geo_obj.ui
+ assert isinstance(ui, GeometryObjectUI) # Just for the IDE
+ ui.generate_cnc_button.click() # Click
+
+ # Work is done in a separate thread and results are
+ # passed via events to the main event loop which is
+ # not running. Run only for pending events.
+ #
+ # I'm not sure why, but running it only once does
+ # not catch the new object. Might be a timing issue.
+ # http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details
+ for _ in range(2):
+ sleep(0.1)
+ self.app.processEvents()
+
+ #---------------------------------------------
+ # Check that only 1 object has been created.
+ #---------------------------------------------
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 2,
+ "Expected 2 objects, found %d" % len(names))
+
+ #-------------------------------------------------------
+ # Make sure the CNC Job Object has the correct name
+ #-------------------------------------------------------
+ cnc_name = geo_name + "_cnc"
+ self.assertTrue(cnc_name in names,
+ "Object named %s not found." % geo_name)
+
+ #-------------------------------------------------------
+ # Get the object make sure it's a CNC Job object
+ #-------------------------------------------------------
+ cnc_obj = self.fc.collection.get_by_name(cnc_name)
+ self.assertTrue(isinstance(cnc_obj, FlatCAMCNCjob),
+ "Expected a FlatCAMCNCJob, got %s" % type(geo_obj))
+
+ #-----------------------------------------
+ # Export G-Code, check output
+ #-----------------------------------------
+ assert isinstance(cnc_obj, FlatCAMCNCjob)
+ output_filename = ""
+ # get system temporary file(try create it and delete also)
+ with tempfile.NamedTemporaryFile(prefix='unittest.',
+ suffix="." + cnc_name + '.gcode',
+ delete=True) as tmp_file:
+ output_filename = tmp_file.name
+ cnc_obj.export_gcode(output_filename)
+ self.assertTrue(os.path.isfile(output_filename))
+ os.remove(output_filename)
+
+ print names
diff --git a/tests/test_tclCommands/__init__.py b/tests/test_tclCommands/__init__.py
new file mode 100644
index 0000000..2d1ed5c
--- /dev/null
+++ b/tests/test_tclCommands/__init__.py
@@ -0,0 +1,18 @@
+import pkgutil
+import sys
+
+# allowed command tests (please append them alphabetically ordered)
+from test_TclCommandAddPolygon import *
+from test_TclCommandAddPolyline import *
+from test_TclCommandCncjob import *
+from test_TclCommandDrillcncjob import *
+from test_TclCommandExportGcode import *
+from test_TclCommandExteriors import *
+from test_TclCommandImportSvg import *
+from test_TclCommandInteriors import *
+from test_TclCommandIsolate import *
+from test_TclCommandNew import *
+from test_TclCommandNewGeometry import *
+from test_TclCommandOpenExcellon import *
+from test_TclCommandOpenGerber import *
+from test_TclCommandPaintPolygon import *
diff --git a/tests/test_tclCommands/test_TclCommandAddPolygon.py b/tests/test_tclCommands/test_TclCommandAddPolygon.py
new file mode 100644
index 0000000..e2099ad
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandAddPolygon.py
@@ -0,0 +1,18 @@
+from FlatCAMObj import FlatCAMGeometry
+
+
+def test_add_polygon(self):
+ """
+ Test add polygon into geometry
+ :param self:
+ :return:
+ """
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ points = '0 0 20 0 10 10 0 10'
+
+ self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points))
diff --git a/tests/test_tclCommands/test_TclCommandAddPolyline.py b/tests/test_tclCommands/test_TclCommandAddPolyline.py
new file mode 100644
index 0000000..69c0577
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandAddPolyline.py
@@ -0,0 +1,18 @@
+from FlatCAMObj import FlatCAMGeometry
+
+
+def test_add_polyline(self):
+ """
+ Test add polyline into geometry
+ :param self:
+ :return:
+ """
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ points = '0 0 20 0 10 10 0 10 33 33'
+
+ self.fc.exec_command_test('add_polyline "%s" %s' % (self.geometry_name, points))
diff --git a/tests/test_tclCommands/test_TclCommandCncjob.py b/tests/test_tclCommands/test_TclCommandCncjob.py
new file mode 100644
index 0000000..cdd8e79
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandCncjob.py
@@ -0,0 +1,17 @@
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMObj
+from test_TclCommandIsolate import *
+
+def test_cncjob(self):
+ """
+ Test cncjob
+ :param self:
+ :return:
+ """
+
+ # reuse isolate tests
+ test_isolate(self)
+
+ self.fc.exec_command_test('cncjob %s_iso -tooldia 0.5 -z_cut 0.05 -z_move 3 -feedrate 300' % self.gerber_top_name)
+ cam_top_obj = self.fc.collection.get_by_name(self.gerber_top_name + '_iso_cnc')
+ self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s"
+ % (self.gerber_top_name + '_iso_cnc', type(cam_top_obj)))
\ No newline at end of file
diff --git a/tests/test_tclCommands/test_TclCommandDrillcncjob.py b/tests/test_tclCommands/test_TclCommandDrillcncjob.py
new file mode 100644
index 0000000..78326d2
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandDrillcncjob.py
@@ -0,0 +1,18 @@
+from FlatCAMObj import FlatCAMObj
+from test_TclCommandOpenExcellon import *
+
+
+def test_drillcncjob(self):
+ """
+ Test cncjob
+ :param self:
+ :return:
+ """
+ # reuse open excellontests
+ test_open_excellon(self)
+
+ self.fc.exec_command_test('drillcncjob %s -tools all -drillz 0.5 -travelz 3 -feedrate 300'
+ % self.excellon_name)
+ cam_top_obj = self.fc.collection.get_by_name(self.excellon_name + '_cnc')
+ self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s"
+ % (self.excellon_name + '_cnc', type(cam_top_obj)))
diff --git a/tests/test_tclCommands/test_TclCommandExportGcode.py b/tests/test_tclCommands/test_TclCommandExportGcode.py
new file mode 100644
index 0000000..102e6e3
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandExportGcode.py
@@ -0,0 +1,33 @@
+import os
+import tempfile
+
+from test_TclCommandCncjob import *
+from test_TclCommandDrillcncjob import *
+
+
+def test_export_gcodecncjob(self):
+ """
+ Test cncjob
+ :param self:
+ :return:
+ """
+
+ # reuse tests
+ test_cncjob(self)
+ test_drillcncjob(self)
+
+ with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.excellon_name + '.gcode', delete=True)\
+ as tmp_file:
+ output_filename = tmp_file.name
+ self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.excellon_name + '_cnc', output_filename))
+ self.assertTrue(os.path.isfile(output_filename))
+ os.remove(output_filename)
+
+ with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.gerber_top_name + '.gcode', delete=True)\
+ as tmp_file:
+ output_filename = tmp_file.name
+ self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.gerber_top_name + '_iso_cnc', output_filename))
+ self.assertTrue(os.path.isfile(output_filename))
+ os.remove(output_filename)
+
+ # TODO check what is inside files , it should be same every time
\ No newline at end of file
diff --git a/tests/test_tclCommands/test_TclCommandExteriors.py b/tests/test_tclCommands/test_TclCommandExteriors.py
new file mode 100644
index 0000000..da47be9
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandExteriors.py
@@ -0,0 +1,24 @@
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
+
+
+def test_exteriors(self):
+ """
+ Test exteriors
+ :param self:
+ :return:
+ """
+
+ 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 interiors and delete isolated traces
+ 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)))
diff --git a/tests/test_tclCommands/test_TclCommandImportSvg.py b/tests/test_tclCommands/test_TclCommandImportSvg.py
new file mode 100644
index 0000000..3db2590
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandImportSvg.py
@@ -0,0 +1,60 @@
+from os import listdir
+
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
+
+
+def test_import_svg(self):
+ """
+ Test all SVG files inside svg directory.
+ Problematic SVG files shold be put there as test reference.
+ :param self:
+ :return:
+ """
+
+ file_list = listdir(self.svg_files)
+
+ for svg_file in file_list:
+
+ # import without outname
+ self.fc.exec_command_test('import_svg "%s/%s"' % (self.svg_files, svg_file))
+
+ obj = self.fc.collection.get_by_name(svg_file)
+ self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (svg_file, type(obj)))
+
+ # import with outname
+ outname = '%s-%s' % (self.geometry_name, svg_file)
+ self.fc.exec_command_test('import_svg "%s/%s" -outname "%s"' % (self.svg_files, svg_file, outname))
+
+ obj = self.fc.collection.get_by_name(outname)
+ self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (outname, type(obj)))
+
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), len(file_list)*2,
+ "Expected %d objects, found %d" % (len(file_list)*2, len(file_list)))
+
+
+def test_import_svg_as_geometry(self):
+
+ self.fc.exec_command_test('import_svg "%s/%s" -type geometry -outname "%s"'
+ % (self.svg_files, self.svg_filename, self.geometry_name))
+
+ obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber),
+ "Expected FlatCAMGeometry, instead, %s is %s" % (self.geometry_name, type(obj)))
+
+
+def test_import_svg_as_gerber(self):
+
+ self.fc.exec_command_test('import_svg "%s/%s" -type gerber -outname "%s"'
+ % (self.svg_files, self.svg_filename, self.gerber_name))
+
+ obj = self.fc.collection.get_by_name(self.gerber_name)
+ self.assertTrue(isinstance(obj, FlatCAMGerber),
+ "Expected FlatCAMGerber, instead, %s is %s" % (self.gerber_name, type(obj)))
+
+ self.fc.exec_command_test('isolate "%s"' % self.gerber_name)
+ obj = self.fc.collection.get_by_name(self.gerber_name+'_iso')
+ self.assertTrue(isinstance(obj, FlatCAMGeometry),
+ "Expected FlatCAMGeometry, instead, %s is %s" % (self.gerber_name+'_iso', type(obj)))
diff --git a/tests/test_tclCommands/test_TclCommandInteriors.py b/tests/test_tclCommands/test_TclCommandInteriors.py
new file mode 100644
index 0000000..c58c380
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandInteriors.py
@@ -0,0 +1,24 @@
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
+
+
+def test_interiors(self):
+ """
+ Test interiors
+ :param self:
+ :return:
+ """
+
+ 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)))
+
+ # interiors and delete isolated traces
+ self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
+ self.fc.exec_command_test('interiors %s -outname %s'
+ % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_interior'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
+ obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_interior')
+ self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.gerber_cutout_name + '_iso_interior', type(obj)))
diff --git a/tests/test_tclCommands/test_TclCommandIsolate.py b/tests/test_tclCommands/test_TclCommandIsolate.py
new file mode 100644
index 0000000..e61aa40
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandIsolate.py
@@ -0,0 +1,21 @@
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
+
+
+def test_isolate(self):
+ """
+ Test isolate gerber
+ :param self:
+ :return:
+ """
+
+ 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)))
+
+ # isolate traces
+ self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter))
+ geometry_top_obj = self.fc.collection.get_by_name(self.gerber_top_name+'_iso')
+ self.assertTrue(isinstance(geometry_top_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.gerber_top_name+'_iso', type(geometry_top_obj)))
\ No newline at end of file
diff --git a/tests/test_tclCommands/test_TclCommandNew.py b/tests/test_tclCommands/test_TclCommandNew.py
new file mode 100644
index 0000000..07eba0b
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandNew.py
@@ -0,0 +1,48 @@
+from FlatCAMObj import FlatCAMGeometry
+
+
+def test_new(self):
+ """
+ Test new project
+ :param self:
+ :return:
+ """
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ self.fc.exec_command_test('proc testproc {} { puts "testresult" }')
+
+ result = self.fc.exec_command_test('testproc')
+
+ self.assertEqual(result, "testresult",'testproc should return "testresult"')
+
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+
+ # object should not exists anymore
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ # TODO after new it should delete all procedures and variables, we need to make sure "testproc" does not exists
+
+ # Test it again with same names
+
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+
+ # object should not exists anymore
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
diff --git a/tests/test_tclCommands/test_TclCommandNewGeometry.py b/tests/test_tclCommands/test_TclCommandNewGeometry.py
new file mode 100644
index 0000000..72e069c
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandNewGeometry.py
@@ -0,0 +1,14 @@
+from FlatCAMObj import FlatCAMGeometry
+
+
+def test_new_geometry(self):
+ """
+ Test create new geometry
+ :param self:
+ :return:
+ """
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
diff --git a/tests/test_tclCommands/test_TclCommandOpenExcellon.py b/tests/test_tclCommands/test_TclCommandOpenExcellon.py
new file mode 100644
index 0000000..7570ea5
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandOpenExcellon.py
@@ -0,0 +1,15 @@
+from FlatCAMObj import FlatCAMExcellon
+
+
+def test_open_excellon(self):
+ """
+ Test open excellon file
+ :param self:
+ :return:
+ """
+
+ 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)))
diff --git a/tests/test_tclCommands/test_TclCommandOpenGerber.py b/tests/test_tclCommands/test_TclCommandOpenGerber.py
new file mode 100644
index 0000000..71d50b0
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandOpenGerber.py
@@ -0,0 +1,25 @@
+from FlatCAMObj import FlatCAMGerber
+
+
+def test_open_gerber(self):
+ """
+ Test open gerber file
+ :param self:
+ :return:
+ """
+
+ 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)))
+
+ #just read with original name
+ self.fc.exec_command_test('open_gerber %s/%s'
+ % (self.gerber_files, self.copper_top_filename))
diff --git a/tests/test_tclCommands/test_TclCommandPaintPolygon.py b/tests/test_tclCommands/test_TclCommandPaintPolygon.py
new file mode 100644
index 0000000..cc0c561
--- /dev/null
+++ b/tests/test_tclCommands/test_TclCommandPaintPolygon.py
@@ -0,0 +1,25 @@
+from FlatCAMObj import FlatCAMGeometry
+
+
+def test_paint_polygon(self):
+ """
+ Test create paint polygon geometry
+ :param self:
+ :return:
+ """
+
+ self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
+ self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ % (self.geometry_name, type(geometry_obj)))
+
+ points = '0 0 20 0 10 10 0 10'
+
+ self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points))
+
+ # TODO rename to paint_polygon in future oop command implementation
+ self.fc.exec_command_test('paint_poly "%s" 5 5 2 0.5' % (self.geometry_name))
+ geometry_obj = self.fc.collection.get_by_name(self.geometry_name+'_paint')
+ # TODO uncoment check after oop implementation, because of threading inside paint poly
+ #self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
+ # % (self.geometry_name+'_paint', type(geometry_obj)))
diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py
new file mode 100644
index 0000000..510a491
--- /dev/null
+++ b/tests/test_tcl_shell.py
@@ -0,0 +1,187 @@
+import sys
+import unittest
+from PyQt4 import QtGui
+from PyQt4.QtCore import QThread
+
+from FlatCAMApp import App
+from os import listdir
+from os.path import isfile
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon
+from ObjectUI import GerberObjectUI, GeometryObjectUI
+from time import sleep
+import os
+import tempfile
+
+
+class TclShellTest(unittest.TestCase):
+
+ svg_files = 'tests/svg'
+ svg_filename = 'Arduino Nano3_pcb.svg'
+ 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'
+ gerber_name = "gerber"
+ geometry_name = "geometry"
+ 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
+
+ # load test methods to split huge test file into smaller pieces
+ # reason for this is reuse one test window only,
+
+ # CANNOT DO THIS HERE!!!
+ #from tests.test_tclCommands import *
+
+ @classmethod
+ def setUpClass(cls):
+
+ cls.setup = True
+ cls.app = QtGui.QApplication(sys.argv)
+ # Create App, keep app defaults (do not load
+ # user-defined defaults).
+ cls.fc = App(user_defaults=False)
+ cls.fc.ui.shell_dock.show()
+
+ def setUp(self):
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.fc.tcl = None
+ cls.app.closeAllWindows()
+ del cls.fc
+ del cls.app
+ pass
+
+ def test_set_get_units(self):
+
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+
+ self.fc.exec_command_test('set_sys units IN')
+ self.fc.exec_command_test('new')
+ units=self.fc.exec_command_test('get_sys units')
+ self.assertEquals(units, "IN")
+
+ self.fc.exec_command_test('set_sys units MM')
+ self.fc.exec_command_test('new')
+ units=self.fc.exec_command_test('get_sys units')
+ self.assertEquals(units, "MM")
+
+ def test_gerber_flow(self):
+
+ # open gerber files top, bottom and cutout
+
+ self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
+ gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
+ self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
+ "Expected FlatCAMGerber, instead, %s is %s" %
+ (self.gerber_top_name, type(gerber_top_obj)))
+
+ self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name))
+ gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
+ self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber),
+ "Expected FlatCAMGerber, instead, %s is %s" %
+ (self.gerber_bottom_name, type(gerber_bottom_obj)))
+
+ self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
+ gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
+ self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber),
+ "Expected FlatCAMGerber, instead, %s is %s" %
+ (self.gerber_cutout_name, type(gerber_cutout_obj)))
+
+ # exteriors delete and join geometries for top layer
+ self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
+ self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
+ obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
+ self.assertTrue(isinstance(obj, FlatCAMGeometry),
+ "Expected FlatCAMGeometry, instead, %s is %s" %
+ (self.gerber_cutout_name + '_iso_exterior', type(obj)))
+
+ # mirror bottom gerbers
+ self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_bottom_name, self.gerber_cutout_name))
+ self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_cutout_name, self.gerber_cutout_name))
+
+ # exteriors delete and join geometries for bottom layer
+ self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.engraver_diameter, self.gerber_cutout_name + '_bottom_iso'))
+ self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
+ obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
+ self.assertTrue(isinstance(obj, FlatCAMGeometry),
+ "Expected FlatCAMGeometry, instead, %s is %s" %
+ (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
+
+ # at this stage we should have 5 objects
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 5,
+ "Expected 5 objects, found %d" % len(names))
+
+ # isolate traces
+ self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter))
+ self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_bottom_name, self.engraver_diameter))
+
+ # join isolated geometries for top and bottom
+ self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_top_name + '_join_iso', self.gerber_top_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
+ self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_bottom_name + '_join_iso', self.gerber_bottom_name + '_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+
+ # at this stage we should have 9 objects
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 9,
+ "Expected 9 objects, found %d" % len(names))
+
+ # clean unused isolations
+ self.fc.exec_command_test('delete %s' % (self.gerber_bottom_name + '_iso'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_top_name + '_iso'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso_exterior'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso_exterior'))
+
+ # at this stage we should have 5 objects again
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 5,
+ "Expected 5 objects, found %d" % len(names))
+
+ # geocutout bottom test (it cuts to same object)
+ self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.cutout_diameter, self.gerber_cutout_name + '_bottom_iso'))
+ self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
+ self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
+ obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
+ self.assertTrue(isinstance(obj, FlatCAMGeometry),
+ "Expected FlatCAMGeometry, instead, %s is %s" %
+ (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
+ self.fc.exec_command_test('geocutout %s -dia %f -gapsize 0.3 -gaps 4' % (self.gerber_cutout_name + '_bottom_iso_exterior', self.cutout_diameter))
+
+ # at this stage we should have 6 objects
+ names = self.fc.collection.get_names()
+ self.assertEqual(len(names), 6,
+ "Expected 6 objects, found %d" % len(names))
+
+ # TODO: tests for tcl
+
+ def test_open_gerber(self):
+
+ self.fc.exec_command_test('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('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