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_header += '' + svg_footer = ' ' + svg_elem = svg_header + exported_svg + svg_footer + + # Parse the xml through a xml parser just to add line feeds + # and to make it look more pretty for the output + doc = parse_xml_string(svg_elem) + with open(filename, 'w') as fp: + fp.write(doc.toprettyxml()) + + def import_svg(self, filename, outname=None): + """ + Adds a new Geometry Object to the projects and populates + 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: - -![copper_clear_1.png](https://bitbucket.org/repo/zbbdpg/images/2313087322-copper_clear_1.png) -![copper_clear_cnc_job_1.png](https://bitbucket.org/repo/zbbdpg/images/1699766214-copper_clear_cnc_job_1.png) -![copper_clear_cnc_job_2.png](https://bitbucket.org/repo/zbbdpg/images/3722929164-copper_clear_cnc_job_2.png) \ 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 @@ + + + + +Fritzing footprint generated by brd2svg + + + + element:J1 + + package:HEAD15-NOSS + + + + element:J2 + + package:HEAD15-NOSS-1 + + + + element:U2 + + package:SSOP28 + + + + element:U3 + + package:SOT223 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + layer 21 + + text:TX1 + + + TX1 + + + + + text:RX0 + + + RX0 + + + + + text:RST + + + RST + + + + + text:GND + + + GND + + + + + text:D2 + + + D2 + + + + + text:D3 + + + D3 + + + + + text:D4 + + + D4 + + + + + text:D5 + + + D5 + + + + + text:D6 + + + D6 + + + + + text:D7 + + + D7 + + + + + text:D8 + + + D8 + + + + + text:D9 + + + D9 + + + + + text:D10 + + + D10 + + + + + text:D11 + + + D11 + + + + + text:D12 + + + D12 + + + + + text:D13 + + + D13 + + + + + text:3V3 + + + 3V3 + + + + + text:REF + + + REF + + + + + text:A0 + + + A0 + + + + + text:A1 + + + A1 + + + + + text:A2 + + + A2 + + + + + text:A3 + + + A3 + + + + + text:A4 + + + A4 + + + + + text:A5 + + + A5 + + + + + text:A6 + + + A6 + + + + + text:A7 + + + A7 + + + + + text:5V + + + 5V + + + + + text:RST + + + RST + + + + + text:GND + + + GND + + + + + text:VIN + + + VIN + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + element:C1 + + package:CAP0805-NP + + + + element:C2 + + package:TAN-A + + + + element:C3 + + package:CAP0805-NP + + + + element:C4 + + package:CAP0805-NP + + + + element:C7 + + package:CAP0805-NP + + + + element:C8 + + package:TAN-A + + + + element:C9 + + package:CAP0805-NP + + + + element:D1 + + package:SOD-123 + + + + element:J1 + + package:HEAD15-NOSS + + + + element:J2 + + package:HEAD15-NOSS-1 + + + + element:RP1 + + package:RES4NT + + + + element:RP2 + + package:RES4NT + + + + element:U$4 + + package:FIDUCIAL-1X2 + + + + element:U$37 + + package:FIDUCIAL-1X2 + + + + element:U$53 + + package:FIDUCIAL-1X2 + + + + element:U$54 + + package:FIDUCIAL-1X2 + + + + element:U2 + + package:SSOP28 + + + + element:U3 + + package:SOT223 + + + + 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + 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