diff --git a/FlatCAM.spec b/FlatCAM.spec deleted file mode 100644 index 73ee23c7..00000000 --- a/FlatCAM.spec +++ /dev/null @@ -1,33 +0,0 @@ -# -*- mode: python -*- - -block_cipher = None - - -a = Analysis(['FlatCAM.py'], - pathex=['/home/jpcaram/flatcam'], - binaries=None, - datas=[('share/*', 'share')], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - exclude_binaries=True, - name='FlatCAM', - debug=False, - strip=False, - upx=True, - console=True ) -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - name='FlatCAM') diff --git a/FlatCAMApp.py b/FlatCAMApp.py deleted file mode 100644 index 3fe46be8..00000000 --- a/FlatCAMApp.py +++ /dev/null @@ -1,4402 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -import sys -import traceback -import urllib.request, urllib.parse, urllib.error -import getopt -import random -import logging -import simplejson as json -import re -import webbrowser -import os -import tkinter -from PyQt4 import Qt, QtCore, QtGui -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 ## -######################################## -import FlatCAMVersion -from FlatCAMWorker import Worker -import ObjectCollection -from FlatCAMObj import FlatCAMCNCjob, FlatCAMExcellon, FlatCAMGerber, FlatCAMGeometry, FlatCAMObj -from PlotCanvas import PlotCanvas -from FlatCAMGUI import FlatCAMGUI, GlobalOptionsUI, FlatCAMActivityView, FlatCAMInfoBar -from FlatCAMCommon import LoudDict -from FlatCAMShell import FCShell -from FlatCAMDraw import FlatCAMDraw -from FlatCAMProcess import * -from GUIElements import FCInputDialog -from ToolMeasurement import Measurement -from ToolDblSided import DblSidedTool -from ToolTransform import ToolTransform -import tclCommands - -from camlib import * - - -######################################## -## App ## -######################################## -class App(QtCore.QObject): - """ - The main application class. The constructor starts the GUI. - """ - - ## Get Cmd Line Options - cmd_line_shellfile = '' - cmd_line_help = "FlatCam.py --shellfile=" - try: - cmd_line_options, args = getopt.getopt(sys.argv[1:], "h:", "shellfile=") - except getopt.GetoptError: - print(cmd_line_help) - sys.exit(2) - for opt, arg in cmd_line_options: - if opt == '-h': - print(cmd_line_help) - sys.exit() - elif opt == '--shellfile': - cmd_line_shellfile = arg - - ## Logging ## - log = logging.getLogger('base') - log.setLevel(logging.DEBUG) - # log.setLevel(logging.WARNING) - formatter = logging.Formatter('[%(levelname)s][%(threadName)s] %(message)s') - handler = logging.StreamHandler() - handler.setFormatter(formatter) - log.addHandler(handler) - - ## Version - version = 8.5 - #version_date_str = "2016/7" - version_date = (0, 0, 0) - version_name = None - - ## URL for update checks and statistics - version_url = "http://flatcam.org/version" - - ## App URL - app_url = "http://flatcam.org" - - ## Manual URL - manual_url = "http://flatcam.org/manual/index.html" - - ################## - ## 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() - # Note: Setting the parameters to unicode does not seem - # to have an effect. Then are received as Qstring - # anyway. - 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 and a plot flag - # on_object_created() adds the object to the collection, plot the object if plot flag is True - # and emits new_object_available. - object_created = QtCore.pyqtSignal(object, bool) - - # 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) - - # 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) - - @property - def version_date_str(self): - return "{:4d}/{:02d}".format( - self.version_date[0], - self.version_date[1] - ) - - def __init__(self, user_defaults=True, post_gui=None): - """ - Starts the application. - - :return: app - :rtype: App - """ - - FlatCAMVersion.setup(self) - - App.log.info("FlatCAM Starting...") - - ################### - ### OS-specific ### - ################### - - # Folder for user settings. - if sys.platform == 'win32': - from win32com.shell import shell, shellcon - App.log.debug("Win32!") - self.data_path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0) + \ - '/FlatCAM' - self.os = 'windows' - else: # Linux/Unix/MacOS - self.data_path = os.path.expanduser('~') + \ - '/.FlatCAM' - self.os = 'unix' - - ############################### - ### Setup folders and files ### - ############################### - - if not os.path.exists(self.data_path): - os.makedirs(self.data_path) - App.log.debug('Created data folder: ' + self.data_path) - - try: - f = open(self.data_path + '/defaults.json') - f.close() - except IOError: - App.log.debug('Creating empty defaults.json') - f = open(self.data_path + '/defaults.json', 'w') - json.dump({}, f) - f.close() - - try: - f = open(self.data_path + '/recent.json') - f.close() - except IOError: - App.log.debug('Creating empty recent.json') - f = open(self.data_path + '/recent.json', 'w') - json.dump([], f) - f.close() - - # Application directory. Chdir to it. Otherwise, trying to load - # GUI icons will fail as thir path is relative. - if hasattr(sys, "frozen"): - # For cx_freeze and sililar. - self.app_home = os.path.dirname(sys.executable) - else: - 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()) - os.chdir(self.app_home) - - #################### - ## Initialize GUI ## - #################### - - QtCore.QObject.__init__(self) - - self.ui = FlatCAMGUI(self.version, name=self.version_name) - self.connect(self.ui, - QtCore.SIGNAL("geomUpdate(int, int, int, int)"), - self.save_geometry) - - #### Plot Area #### - # self.plotcanvas = PlotCanvas(self.ui.splitter) - 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) - - self.ui.splitter.setStretchFactor(1, 2) - - ############## - #### Data #### - ############## - self.recent = [] - - self.clipboard = QtGui.QApplication.clipboard() - - self.proc_container = FCVisibleProcessContainer(self.ui.activity_view) - - self.project_filename = None - - self.toggle_units_ignore = False - - self.defaults_form = GlobalOptionsUI() - self.defaults_form_fields = { - "units": self.defaults_form.units_radio, - "gerber_plot": self.defaults_form.gerber_group.plot_cb, - "gerber_solid": self.defaults_form.gerber_group.solid_cb, - "gerber_multicolored": self.defaults_form.gerber_group.multicolored_cb, - "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_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, - "gerber_cutoutgapsize": self.defaults_form.gerber_group.cutout_gap_entry, - "gerber_gaps": self.defaults_form.gerber_group.gaps_radio, - "gerber_noncoppermargin": self.defaults_form.gerber_group.noncopper_margin_entry, - "gerber_noncopperrounded": self.defaults_form.gerber_group.noncopper_rounded_cb, - "gerber_bboxmargin": self.defaults_form.gerber_group.bbmargin_entry, - "gerber_bboxrounded": self.defaults_form.gerber_group.bbrounded_cb, - "excellon_plot": self.defaults_form.excellon_group.plot_cb, - "excellon_solid": self.defaults_form.excellon_group.solid_cb, - "excellon_drillz": self.defaults_form.excellon_group.cutz_entry, - "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, - "geometry_feedrate": self.defaults_form.geometry_group.cncfeedrate_entry, - "geometry_cnctooldia": self.defaults_form.geometry_group.cnctooldia_entry, - "geometry_painttooldia": self.defaults_form.geometry_group.painttooldia_entry, - "geometry_spindlespeed": self.defaults_form.geometry_group.cncspindlespeed_entry, - "geometry_paintoverlap": self.defaults_form.geometry_group.paintoverlap_entry, - "geometry_paintmargin": self.defaults_form.geometry_group.paintmargin_entry, - "geometry_selectmethod": self.defaults_form.geometry_group.selectmethod_combo, - "geometry_pathconnect": self.defaults_form.geometry_group.pathconnect_cb, - "geometry_paintcontour": self.defaults_form.geometry_group.contour_cb, - "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_dwell": self.defaults_form.cncjob_group.dwell_cb, - "cncjob_dwelltime": self.defaults_form.cncjob_group.dwelltime_cb - } - - self.defaults = LoudDict() - self.defaults.set_change_callback(self.on_defaults_dict_change) # When the dictionary changes. - self.defaults.update({ - "global_mouse_pan_button": 2, - "serial": 0, - "stats": {}, - "units": "IN", - "gerber_plot": True, - "gerber_solid": True, - "gerber_multicolored": False, - "gerber_isotooldia": 0.016, - "gerber_isopasses": 1, - "gerber_isooverlap": 0.15, - "gerber_cutouttooldia": 0.07, - "gerber_cutoutmargin": 0.1, - "gerber_cutoutgapsize": 0.15, - "gerber_gaps": "4", - "gerber_noncoppermargin": 0.0, - "gerber_noncopperrounded": False, - "gerber_bboxmargin": 0.0, - "gerber_bboxrounded": False, - "excellon_plot": True, - "excellon_solid": False, - "excellon_drillz": -0.1, - "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, - "geometry_feedrate": 3.0, - "geometry_cnctooldia": 0.016, - "geometry_spindlespeed": None, - "geometry_painttooldia": 0.07, - "geometry_paintoverlap": 0.15, - "geometry_paintmargin": 0.0, - "geometry_selectmethod": "single", - "geometry_pathconnect": True, - "geometry_paintcontour": True, - "cncjob_plot": True, - "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, - # Default window geometry - "def_win_x": 100, - "def_win_y": 100, - "def_win_w": 1024, - "def_win_h": 650, - - # Constants... - "defaults_save_period_ms": 20000, # Time between default saves. - "shell_shape": [500, 300], # Shape of the shell in pixels. - "shell_at_startup": False, # Show the shell at startup. - "recent_limit": 10, # Max. items in recent list. - "fit_key": '1', - "zoom_out_key": '2', - "zoom_in_key": '3', - "zoom_ratio": 1.5, - "point_clipboard_format": "(%.4f, %.4f)", - "zdownrate": None, - "excellon_zeros": "L", - "gerber_use_buffer_for_union": True, - "cncjob_coordinate_format": "X%.4fY%.4f" - }) - - ############################### - ### Load defaults from file ### - if user_defaults: - self.load_defaults() - - chars = 'abcdefghijklmnopqrstuvwxyz0123456789' - if self.defaults['serial'] == 0 or len(str(self.defaults['serial'])) < 10: - self.defaults['serial'] = ''.join([random.choice(chars) for i in range(20)]) - self.save_defaults(silent=True) - - self.propagate_defaults() - self.restore_main_win_geom() - - def auto_save_defaults(): - try: - self.save_defaults(silent=True) - finally: - QtCore.QTimer.singleShot(self.defaults["defaults_save_period_ms"], auto_save_defaults) - - if user_defaults: - 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, - "gerber_solid": self.options_form.gerber_group.solid_cb, - "gerber_multicolored": self.options_form.gerber_group.multicolored_cb, - "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_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, - "gerber_cutoutgapsize": self.options_form.gerber_group.cutout_gap_entry, - "gerber_gaps": self.options_form.gerber_group.gaps_radio, - "gerber_noncoppermargin": self.options_form.gerber_group.noncopper_margin_entry, - "gerber_noncopperrounded": self.options_form.gerber_group.noncopper_rounded_cb, - "gerber_bboxmargin": self.options_form.gerber_group.bbmargin_entry, - "gerber_bboxrounded": self.options_form.gerber_group.bbrounded_cb, - "excellon_plot": self.options_form.excellon_group.plot_cb, - "excellon_solid": self.options_form.excellon_group.solid_cb, - "excellon_drillz": self.options_form.excellon_group.cutz_entry, - "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, - "geometry_feedrate": self.options_form.geometry_group.cncfeedrate_entry, - "geometry_spindlespeed": self.options_form.geometry_group.cncspindlespeed_entry, - "geometry_cnctooldia": self.options_form.geometry_group.cnctooldia_entry, - "geometry_painttooldia": self.options_form.geometry_group.painttooldia_entry, - "geometry_paintoverlap": self.options_form.geometry_group.paintoverlap_entry, - "geometry_paintmargin": self.options_form.geometry_group.paintmargin_entry, - "geometry_selectmethod": self.options_form.geometry_group.selectmethod_combo, - "cncjob_plot": self.options_form.cncjob_group.plot_cb, - "cncjob_tooldia": self.options_form.cncjob_group.tooldia_entry, - "cncjob_prepend": self.options_form.cncjob_group.prepend_text, - "cncjob_append": self.options_form.cncjob_group.append_text - } - - self.options = LoudDict() - self.options.set_change_callback(self.on_options_dict_change) - self.options.update({ - "units": "IN", - "gerber_plot": True, - "gerber_solid": True, - "gerber_multicolored": False, - "gerber_isotooldia": 0.016, - "gerber_isopasses": 1, - "gerber_isooverlap": 0.15, - "gerber_combine_passes": True, - "gerber_cutouttooldia": 0.07, - "gerber_cutoutmargin": 0.1, - "gerber_cutoutgapsize": 0.15, - "gerber_gaps": "4", - "gerber_noncoppermargin": 0.0, - "gerber_noncopperrounded": False, - "gerber_bboxmargin": 0.0, - "gerber_bboxrounded": False, - "excellon_plot": True, - "excellon_solid": False, - "excellon_drillz": -0.1, - "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, - "geometry_feedrate": 3.0, - "geometry_spindlespeed": None, - "geometry_cnctooldia": 0.016, - "geometry_painttooldia": 0.07, - "geometry_paintoverlap": 0.15, - "geometry_paintmargin": 0.0, - "geometry_selectmethod": "single", - "cncjob_plot": True, - "cncjob_tooldia": 0.016, - "cncjob_prepend": "", - "cncjob_append": "", - "background_timeout": 300000, # Default value is 5 minutes - "verbose_error_level": 0, # Shell verbosity: - # 0 = default(python trace only for unknown errors), - # 1 = show trace(show trace allways), 2 = (For the future). - }) - self.options.update(self.defaults) # Copy app defaults to project options - #self.options_write_form() - self.on_options_combo_change(0) # Will show the initial form - - self.collection = ObjectCollection.ObjectCollection() - self.ui.project_tab_layout.addWidget(self.collection.view) - - self.mouse_pan_button = int(self.defaults['global_mouse_pan_button']) - #### End of Data #### - - #### Worker #### - App.log.info("Starting Worker...") - self.worker = Worker(self) - self.thr1 = QtCore.QThread() - self.worker.moveToThread(self.thr1) - self.connect(self.thr1, QtCore.SIGNAL("started()"), self.worker.run) - self.thr1.start() - - #### Check for updates #### - # Separate thread (Not worker) - App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version)) - - self.worker2 = Worker(self, name="worker2") - self.thr2 = QtCore.QThread() - self.worker2.moveToThread(self.thr2) - self.connect(self.thr2, QtCore.SIGNAL("started()"), self.worker2.run) - self.connect(self.thr2, QtCore.SIGNAL("started()"), - lambda: self.worker_task.emit({'fcn': self.version_check, - 'params': [], - 'worker_name': "worker2"})) - self.thr2.start() - - ### Signal handling ### - ## Custom signals - self.inform.connect(self.info) - self.message.connect(self.message_dialog) - self.progress.connect(self.set_progress_bar) - self.object_created.connect(self.on_object_created) - self.plots_updated.connect(self.on_plots_updated) - self.file_opened.connect(self.register_recent) - self.file_opened.connect(lambda kind, filename: self.register_folder(filename)) - ## Standard signals - # Menu - self.ui.menufilenew.triggered.connect(self.on_file_new) - self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber) - 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)) - self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults) - self.ui.exit_action.triggered.connect(self.on_file_exit) - self.ui.menueditnew.triggered.connect(lambda: self.new_object('geometry', 'New Geometry', lambda x, y: None)) - self.ui.menueditedit.triggered.connect(self.edit_geometry) - self.ui.menueditok.triggered.connect(self.editor2geometry) - self.ui.menueditjoin.triggered.connect(self.on_edit_join) - self.ui.menueditdelete.triggered.connect(self.on_delete) - self.ui.menuoptions_transfer_a2o.triggered.connect(self.on_options_app2object) - self.ui.menuoptions_transfer_a2p.triggered.connect(self.on_options_app2project) - self.ui.menuoptions_transfer_o2a.triggered.connect(self.on_options_object2app) - self.ui.menuoptions_transfer_p2a.triggered.connect(self.on_options_project2app) - self.ui.menuoptions_transfer_o2p.triggered.connect(self.on_options_object2project) - self.ui.menuoptions_transfer_p2o.triggered.connect(self.on_options_project2object) - 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(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)) - # Toolbar - self.ui.open_gerber_btn.triggered.connect(self.on_fileopengerber) - self.ui.open_exc_btn.triggered.connect(self.on_fileopenexcellon) - self.ui.open_gcode_btn.triggered.connect(self.on_fileopengcode) - self.ui.save_btn.triggered.connect(self.on_file_saveprojectas) - self.ui.zoom_fit_btn.triggered.connect(self.on_zoom_fit) - self.ui.zoom_in_btn.triggered.connect(lambda: self.plotcanvas.zoom(1.5)) - self.ui.zoom_out_btn.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5)) - self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear) - self.ui.replot_btn.triggered.connect(self.on_toolbar_replot) - self.ui.newgeo_btn.triggered.connect(lambda: self.new_object('geometry', 'New Geometry', lambda x, y: None)) - 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(self.on_toggle_shell) - # Object list - self.collection.view.activated.connect(self.on_row_activated) - # Options - self.ui.options_combo.activated.connect(self.on_options_combo_change) - self.options_form.units_radio.group_toggle_fn = self.on_toggle_units - #Notebook tabs - - #################### - ### Other setups ### - #################### - # Sets up FlatCAMObj, FCProcess and FCProcessContainer. - self.setup_obj_classes() - - self.setup_recent_items() - self.setup_component_editor() - - ######################### - ### Tools and Plugins ### - ######################### - self.dblsidedtool = DblSidedTool(self) - self.dblsidedtool.install(icon=QtGui.QIcon('share/doubleside16.png'), separator=True) - - self.measurement_tool = Measurement(self) - self.measurement_tool.install(icon=QtGui.QIcon('share/measure16.png')) - self.ui.measure_btn.triggered.connect(self.measurement_tool.run) - - self.transform_tool = ToolTransform(self) - self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuedit) - - self.draw = FlatCAMDraw(self, disabled=True) - - ############# - ### Shell ### - ############# - # TODO: Move this to its own class - - self.shell = FCShell(self) - self.shell.setWindowIcon(self.ui.app_icon) - self.shell.setWindowTitle("FlatCAM Shell") - self.shell.resize(*self.defaults["shell_shape"]) - self.shell.append_output("FlatCAM {}".format(self.version)) - if self.version_name: - self.shell.append_output(" - {}".format(self.version_name)) - self.shell.append_output("\n(c) 2014-{} Juan Pablo Caram\n\n".format( - self.version_date[0])) - self.shell.append_output("Type help to get started.\n\n") - - self.init_tcl() - - self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") - self.ui.shell_dock.setWidget(self.shell) - self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) - self.ui.shell_dock.setFeatures(QtGui.QDockWidget.DockWidgetMovable | - QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetClosable) - self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) - - - if self.defaults["shell_at_startup"]: - self.ui.shell_dock.show() - else: - self.ui.shell_dock.hide() - - if self.cmd_line_shellfile: - try: - with open(self.cmd_line_shellfile, "r") as myfile: - cmd_line_shellfile_text = myfile.read() - self.shell._sysShell.exec_command(cmd_line_shellfile_text) - except Exception as ext: - print(("ERROR: ", ext)) - sys.exit(2) - - # Post-GUI initialization: Experimental attempt - # to perform unit tests on the GUI. - if post_gui is not None: - post_gui(self) - - 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() - - def defaults_write_form(self): - for option in self.defaults: - self.defaults_write_form_field(option) - # try: - # self.defaults_form_fields[option].set_value(self.defaults[option]) - # except KeyError: - # #self.log.debug("defaults_write_form(): No field for: %s" % option) - # # TODO: Rethink this? - # pass - - def defaults_write_form_field(self, field): - try: - self.defaults_form_fields[field].set_value(self.defaults[field]) - except KeyError: - #self.log.debug("defaults_write_form(): No field for: %s" % option) - # TODO: Rethink this? - pass - - def disable_plots(self, except_current=False): - """ - Disables all plots with exception of the current object if specified. - - :param except_current: Wether to skip the current object. - :rtype except_current: boolean - :return: None - """ - # TODO: This method is very similar to replot_all. Try to merge. - self.progress.emit(10) - - def worker_task(app_obj): - percentage = 0.1 - try: - delta = 0.9 / len(self.collection.get_list()) - except ZeroDivisionError: - self.progress.emit(0) - return - for obj in self.collection.get_list(): - if obj != self.collection.get_active() or not except_current: - obj.options['plot'] = False - obj.plot() - percentage += delta - self.progress.emit(int(percentage*100)) - - self.progress.emit(0) - self.plots_updated.emit() - - # Send to worker - self.worker_task.emit({'fcn': worker_task, 'params': [self]}) - - def edit_geometry(self): - """ - Send the current geometry object (if any) into the editor. - - :return: None - """ - if not isinstance(self.collection.get_active(), FlatCAMGeometry): - self.inform.emit("Select a Geometry Object to edit.") - return - - self.ui.updategeo_btn.setEnabled(True) - - self.draw.edit_fcgeometry(self.collection.get_active()) - - def editor2geometry(self): - """ - Transfers the geometry in the editor to the current geometry object. - - :return: None - """ - geo = self.collection.get_active() - if not isinstance(geo, FlatCAMGeometry): - self.inform.emit("Select a Geometry Object to update.") - return - - self.draw.update_fcgeometry(geo) - self.draw.deactivate() - - self.ui.updategeo_btn.setEnabled(False) - - geo.plot() - - def get_last_folder(self): - return self.defaults["last_folder"] - - def report_usage(self, resource): - """ - Increments usage counter for the given resource - in self.defaults['stats']. - - :param resource: Name of the resource. - :return: None - """ - - if resource in self.defaults['stats']: - self.defaults['stats'][resource] += 1 - 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): - """ - Same as exec_command(...) with additional control over exceptions. - Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. - - :param text: Input command - :param reraise: Re-raise TclError exceptions in Python (mostly for unitttests). - :return: Output from the command - """ - - text = str(text) - - try: - self.shell.open_proccessing() # Disables input box. - result = self.tcl.eval(str(text)) - if result != 'None': - self.shell.append_output(result + '\n') - - except tkinter.TclError as e: - # This will display more precise answer if something in TCL shell fails - 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. - """ - - parts = re.findall(r'([\w\\:\.]+|".*?")+', text) - parts = [p.replace('\n', '').replace('"', '') for p in parts] - self.log.debug(parts) - try: - if parts[0] not in commands: - self.shell.append_error("Unknown command\n") - return - - #import inspect - #inspect.getargspec(someMethod) - if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \ - (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]): - self.shell.append_error( - "Command %s takes %d arguments. %d given.\n" % - (parts[0], commands[parts[0]]["params"], len(parts)-1) - ) - return - - cmdfcn = commands[parts[0]]["fcn"] - cmdconv = commands[parts[0]]["converters"] - if len(parts) - 1 > 0: - retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)]) - else: - retval = cmdfcn() - retfcn = commands[parts[0]]["retfcn"] - if retval and retfcn(retval): - self.shell.append_output(retfcn(retval) + "\n") - - except Exception as e: - #self.shell.append_error(''.join(traceback.format_exc())) - #self.shell.append_error("?\n") - self.shell.append_error(str(e) + "\n") - - def info(self, msg, toshell=True): - """ - Informs the user. Normally on the status bar, optionally - also on the shell. - - :param msg: Text to write. - :param toshell: Forward the meesage to the shell. - :return: None - """ - - # Type of message in brackets at the begining of the message. - match = re.search("\[([^\]]+)\](.*)", msg) - if match: - level = match.group(1) - msg_ = match.group(2) - self.ui.fcinfo.set_status(str(msg_), level=level) - - if toshell: - error = level == "error" or level == "warning" - self.shell_message(msg, error=error, show=True) - - else: - self.ui.fcinfo.set_status(str(msg), level="info") - - if toshell: - self.shell_message(msg) - - def load_defaults(self): - """ - Loads the aplication's default settings from defaults.json into - ``self.defaults``. - - :return: None - """ - try: - f = open(self.data_path + "/defaults.json") - options = f.read() - f.close() - except IOError: - self.log.error("Could not load defaults file.") - self.inform.emit("ERROR: Could not load defaults file.") - return - - try: - defaults = json.loads(options) - except: - e = sys.exc_info()[0] - App.log.error(str(e)) - self.inform.emit("ERROR: Failed to parse defaults file.") - return - self.defaults.update(defaults) - - def save_geometry(self, x, y, width, height): - self.defaults["def_win_x"] = x - self.defaults["def_win_y"] = y - self.defaults["def_win_w"] = width - self.defaults["def_win_h"] = height - self.save_defaults() - - def message_dialog(self, title, message, kind="info"): - icon = {"info": QtGui.QMessageBox.Information, - "warning": QtGui.QMessageBox.Warning, - "error": QtGui.QMessageBox.Critical}[str(kind)] - dlg = QtGui.QMessageBox(icon, title, message, parent=self.ui) - dlg.setText(message) - dlg.exec_() - - def register_recent(self, kind, filename): - - self.log.debug("register_recent()") - self.log.debug(" %s" % kind) - self.log.debug(" %s" % filename) - - record = {'kind': str(kind), 'filename': str(filename)} - if record in self.recent: - return - - self.recent.insert(0, record) - - if len(self.recent) > self.defaults['recent_limit']: # Limit reached - self.recent.pop() - - try: - f = open(self.data_path + '/recent.json', 'w') - except IOError: - App.log.error("Failed to open recent items file for writing.") - self.inform.emit('Failed to open recent files file for writing.') - return - - #try: - json.dump(self.recent, f) - # except: - # App.log.error("Failed to write to recent items file.") - # self.inform.emit('ERROR: Failed to write to recent items file.') - # f.close() - - f.close() - - # Re-buid the recent items menu - self.setup_recent_items() - - def new_object(self, kind, name, initialize, active=True, fit=True, plot=True): - """ - Creates a new specalized FlatCAMObj and attaches it to the application, - this is, updates the GUI accordingly, any other records and plots it. - This method is thread-safe. - - Notes: - * If the name is in use, the self.collection will modify it - when appending it to the collection. There is no need to handle - name conflicts here. - - :param kind: The kind of object to create. One of 'gerber', - 'excellon', 'cncjob' and 'geometry'. - :type kind: str - :param name: Name for the object. - :type name: str - :param initialize: Function to run after creation of the object - but before it is attached to the application. The function is - called with 2 parameters: the new object and the App instance. - :type initialize: function - :param plot: Whether to plot the object or not - :type plot: Bool - :return: None - :rtype: None - """ - self.plot = plot - - App.log.debug("new_object()") - - t0 = time.time() # Debug - - ## Create object - classdict = { - "gerber": FlatCAMGerber, - "excellon": FlatCAMExcellon, - "cncjob": FlatCAMCNCjob, - "geometry": FlatCAMGeometry - } - - App.log.debug("Calling object constructor...") - obj = classdict[kind](name) - obj.units = self.options["units"] # TODO: The constructor should look at defaults. - - # Set default options from self.options - for option in self.options: - if option.find(kind + "_") == 0: - oname = option[len(kind) + 1:] - obj.options[oname] = self.options[option] - - # make sure that the plot option of the new object is reflecting the current status and not the general option - # solve issues with the modelview currently used (checkbox on the Project Tab) - obj.options['plot'] = self.plot - - # Initialize as per user request - # User must take care to implement initialize - # in a thread-safe way as is is likely that we - # have been invoked in a separate thread. - t1 = time.time() - self.log.debug("%f seconds before initialize()." % (t1 - t0)) - initialize(obj, self) - t2 = time.time() - self.log.debug("%f seconds executing initialize()." % (t2 - t1)) - - # Check units and convert if necessary - # This condition CAN be true because initialize() can change obj.units - if self.options["units"].upper() != obj.units.upper(): - self.inform.emit("Converting units to " + self.options["units"] + ".") - obj.convert_units(self.options["units"]) - t3 = time.time() - self.log.debug("%f seconds converting units." % (t3 - t2)) - - self.log.debug("Moving new object back to main thread.") - - # Move the object to the main thread and let the app know that it is available. - obj.moveToThread(QtGui.QApplication.instance().thread()) - self.object_created.emit(obj, self.plot) - - return obj - - def options_read_form(self): - for option in self.options_form_fields: - self.options[option] = self.options_form_fields[option].get_value() - - def options_write_form(self): - for option in self.options: - self.options_write_form_field(option) - - def options_write_form_field(self, field): - try: - self.options_form_fields[field].set_value(self.options[field]) - except KeyError: - # Changed from error to debug. This allows to have data stored - # which is not user-editable. - self.log.debug("options_write_form_field(): No field for: %s" % field) - - def on_about(self): - """ - Displays the "about" dialog. - - :return: None - """ - self.report_usage("on_about") - - version = self.version - version_date_str = self.version_date_str - version_year = self.version_date[0] - - class AboutDialog(QtGui.QDialog): - def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) - - # Icon and title - self.setWindowIcon(parent.app_icon) - self.setWindowTitle("FlatCAM") - - layout1 = QtGui.QVBoxLayout() - self.setLayout(layout1) - - layout2 = QtGui.QHBoxLayout() - layout1.addLayout(layout2) - - logo = QtGui.QLabel() - logo.setPixmap(QtGui.QPixmap('share/flatcam_icon256.png')) - layout2.addWidget(logo, stretch=0) - - title = QtGui.QLabel( - "FlatCAM
" - "Version {} ({})
" - "
" - "2D Computer-Aided Printed Circuit Board
" - "Manufacturing.
" - "
" - "(c) 2014-{} Juan Pablo Caram".format( - version, - version_date_str, - version_year - ) - ) - layout2.addWidget(title, stretch=1) - - layout3 = QtGui.QHBoxLayout() - layout1.addLayout(layout3) - layout3.addStretch() - okbtn = QtGui.QPushButton("Close") - layout3.addWidget(okbtn) - - okbtn.clicked.connect(self.accept) - - AboutDialog(self.ui).exec_() - - def on_file_savedefaults(self): - """ - Callback for menu item File->Save Defaults. Saves application default options - ``self.defaults`` to defaults.json. - - :return: None - """ - - self.save_defaults() - - def on_file_exit(self): - QtGui.qApp.quit() - - def save_defaults(self, silent=False): - """ - Saves application default options - ``self.defaults`` to defaults.json. - - :return: None - """ - - self.report_usage("save_defaults") - - ## Read options from file ## - try: - f = open(self.data_path + "/defaults.json") - options = f.read() - f.close() - except: - e = sys.exc_info()[0] - App.log.error("Could not load defaults file.") - App.log.error(str(e)) - self.inform.emit("[error] Could not load defaults file.") - return - - try: - defaults = json.loads(options) - except: - e = sys.exc_info()[0] - App.log.error("Failed to parse defaults file.") - App.log.error(str(e)) - self.inform.emit("[error] Failed to parse defaults file.") - return - - # Update options - self.defaults_read_form() - defaults.update(self.defaults) - - # Save update options - try: - f = open(self.data_path + "/defaults.json", "w") - json.dump(defaults, f) - f.close() - except: - self.inform.emit("[error] Failed to write defaults to file.") - return - - 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 - a new one. - - :return: None - """ - - objs = self.collection.get_selected() - - def initialize(obj, app): - FlatCAMGeometry.merge(objs, obj) - - self.new_object("geometry", "Combo", initialize) - - def on_options_app2project(self): - """ - Callback for Options->Transfer Options->App=>Project. Copies options - from application defaults to project defaults. - - :return: None - """ - - self.report_usage("on_options_app2project") - - self.defaults_read_form() - self.options.update(self.defaults) - self.options_write_form() - - def on_options_project2app(self): - """ - Callback for Options->Transfer Options->Project=>App. Copies options - from project defaults to application defaults. - - :return: None - """ - - self.report_usage("on_options_project2app") - - self.options_read_form() - self.defaults.update(self.options) - self.defaults_write_form() - - def on_options_project2object(self): - """ - Callback for Options->Transfer Options->Project=>Object. Copies options - from project defaults to the currently selected object. - - :return: None - """ - - self.report_usage("on_options_project2object") - - self.options_read_form() - obj = self.collection.get_active() - if obj is None: - self.inform.emit("WARNING: No object selected.") - return - for option in self.options: - if option.find(obj.kind + "_") == 0: - oname = option[len(obj.kind)+1:] - obj.options[oname] = self.options[option] - obj.to_form() # Update UI - - def on_options_object2project(self): - """ - Callback for Options->Transfer Options->Object=>Project. Copies options - from the currently selected object to project defaults. - - :return: None - """ - - self.report_usage("on_options_object2project") - - obj = self.collection.get_active() - if obj is None: - self.inform.emit("WARNING: No object selected.") - return - obj.read_form() - for option in obj.options: - if option in ['name']: # TODO: Handle this better... - continue - self.options[obj.kind + "_" + option] = obj.options[option] - self.options_write_form() - - def on_options_object2app(self): - """ - Callback for Options->Transfer Options->Object=>App. Copies options - from the currently selected object to application defaults. - - :return: None - """ - - self.report_usage("on_options_object2app") - - obj = self.collection.get_active() - if obj is None: - self.inform.emit("WARNING: No object selected.") - return - obj.read_form() - for option in obj.options: - if option in ['name']: # TODO: Handle this better... - continue - self.defaults[obj.kind + "_" + option] = obj.options[option] - self.defaults_write_form() - - def on_options_app2object(self): - """ - Callback for Options->Transfer Options->App=>Object. Copies options - from application defaults to the currently selected object. - - :return: None - """ - - self.report_usage("on_options_app2object") - - self.defaults_read_form() - obj = self.collection.get_active() - if obj is None: - self.inform.emit("WARNING: No object selected.") - return - for option in self.defaults: - if option.find(obj.kind + "_") == 0: - oname = option[len(obj.kind)+1:] - obj.options[oname] = self.defaults[option] - obj.to_form() # Update UI - - def on_options_dict_change(self, field): - self.options_write_form_field(field) - - if field == "units": - self.set_screen_units(self.options['units']) - - def on_defaults_dict_change(self, field): - self.defaults_write_form_field(field) - - def set_screen_units(self, units): - self.ui.units_label.setText("[" + self.options["units"].lower() + "]") - - def on_toggle_units(self): - """ - Callback for the Units radio-button change in the Options tab. - Changes the application's default units or the current project's units. - If changing the project's units, the change propagates to all of - the objects in the project. - - :return: None - """ - - self.report_usage("on_toggle_units") - - if self.toggle_units_ignore: - return - - # If option is the same, then ignore - if self.options_form.units_radio.get_value().upper() == self.options['units'].upper(): - self.log.debug("on_toggle_units(): Same as options, so ignoring.") - return - - # Options to scale - dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize', - 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz', - 'excellon_travelz', 'excellon_feedrate', 'excellon_toolchangez', 'excellon_tooldia', 'cncjob_tooldia', - 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate', - 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap', - 'geometry_paintmargin'] - - def scale_options(sfactor): - for dim in dimensions: - self.options[dim] *= sfactor - - # The scaling factor depending on choice of units. - factor = 1/25.4 - if self.options_form.units_radio.get_value().upper() == 'MM': - factor = 25.4 - - # Changing project units. Warn user. - msgbox = QtGui.QMessageBox() - msgbox.setText("Change project units ...") - msgbox.setInformativeText("Changing the units of the project causes all geometrical " - "properties of all objects to be scaled accordingly. Continue?") - msgbox.setStandardButtons(QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) - msgbox.setDefaultButton(QtGui.QMessageBox.Ok) - - response = msgbox.exec_() - - if response == QtGui.QMessageBox.Ok: - self.options_read_form() - scale_options(factor) - self.options_write_form() - for obj in self.collection.get_list(): - units = self.options_form.units_radio.get_value().upper() - obj.convert_units(units) - current = self.collection.get_active() - if current is not None: - current.to_form() - self.plot_all() - else: - # Undo toggling - self.toggle_units_ignore = True - if self.options_form.units_radio.get_value().upper() == 'MM': - self.options_form.units_radio.set_value('IN') - else: - self.options_form.units_radio.set_value('MM') - self.toggle_units_ignore = False - - self.options_read_form() - self.inform.emit("Converted units to %s" % self.options["units"]) - #self.ui.units_label.setText("[" + self.options["units"] + "]") - self.set_screen_units(self.options["units"]) - - def on_options_combo_change(self, sel): - """ - Called when the combo box to choose between application defaults and - project option changes value. The corresponding variables are - copied to the UI. - - :param sel: The option index that was chosen. - :return: None - """ - - # combo_sel = self.ui.notebook.combo_options.get_active() - App.log.debug("Options --> %s" % sel) - - # Remove anything else in the box - # box_children = self.options_box.get_children() - # box_children = self.ui.notebook.options_contents.get_children() - # for child in box_children: - # self.ui.notebook.options_contents.remove(child) - - # try: - # self.ui.options_area.removeWidget(self.defaults_form) - # except: - # pass - # - # try: - # self.ui.options_area.removeWidget(self.options_form) - # except: - # pass - - form = [self.defaults_form, self.options_form][sel] - # self.ui.notebook.options_contents.pack_start(form, False, False, 1) - try: - self.ui.options_scroll_area.takeWidget() - except: - self.log.debug("Nothing to remove") - self.ui.options_scroll_area.setWidget(form) - form.show() - - # self.options2form() - - def on_delete(self): - """ - Delete the currently selected FlatCAMObjs. - - :return: None - """ - - self.log.debug("on_delete()") - self.report_usage("on_delete") - - while (self.collection.get_active()): - self.delete_first_selected() - - def delete_first_selected(self): - # Keep this for later - try: - name = self.collection.get_active().options["name"] - isPlotted = self.collection.get_active().options["plot"] - except AttributeError: - self.log.debug("Nothing selected for deletion") - return - - # Remove plot only if the object was plotted otherwise delaxes will fail - if isPlotted: - self.plotcanvas.figure.delaxes(self.collection.get_active().axes) - self.plotcanvas.auto_adjust_axes() - - # Clear form - self.setup_component_editor() - - # Remove from dictionary - self.collection.delete_active() - - self.inform.emit("Object deleted: %s" % name) - - def on_plots_updated(self): - """ - Callback used to report when the plots have changed. - Adjust axes and zooms to fit. - - :return: None - """ - self.plotcanvas.auto_adjust_axes() - self.on_zoom_fit(None) - - def on_toolbar_replot(self): - """ - Callback for toolbar button. Re-plots all objects. - - :return: None - """ - - self.report_usage("on_toolbar_replot") - self.log.debug("on_toolbar_replot()") - - try: - self.collection.get_active().read_form() - except AttributeError: - self.log.debug("on_toolbar_replot(): AttributeError") - pass - - self.plot_all() - - def on_row_activated(self, index): - self.ui.notebook.setCurrentWidget(self.ui.selected_tab) - - def on_object_created(self, obj, plot): - """ - Event callback for object creation. - - :param obj: The newly created FlatCAM object. - :param plot: If to plot the new object, bool - :return: None - """ - t0 = time.time() # DEBUG - self.log.debug("on_object_created()") - - # The Collection might change the name if there is a collision - self.collection.append(obj) - - self.inform.emit("Object (%s) created: %s" % (obj.kind, obj.options['name'])) - self.new_object_available.emit(obj) - if plot: - obj.plot() - - # deselect all previously selected objects and select the new one - self.collection.set_all_inactive() - name = obj.options['name'] - self.collection.set_active(name) - - self.on_zoom_fit(None) - t1 = time.time() # DEBUG - self.log.debug("%f seconds adding object and plotting." % (t1 - t0)) - - def on_zoom_fit(self, event): - """ - Callback for zoom-out request. This can be either from the corresponding - toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()`` - with axes limits from the geometry bounds of all objects. - - :param event: Ignored. - :return: None - """ - - xmin, ymin, xmax, ymax = self.collection.get_bounds() - width = xmax - xmin - height = ymax - ymin - xmin -= 0.05 * width - xmax += 0.05 * width - ymin -= 0.05 * height - ymax += 0.05 * height - self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax) - - def on_key_over_plot(self, event): - """ - Callback for the key pressed event when the canvas is focused. Keyboard - shortcuts are handled here. So far, these are the shortcuts: - - ========== ============================================ - Key Action - ========== ============================================ - '1' Zoom-fit. Fits the axes limits to the data. - '2' Zoom-out. - '3' Zoom-in. - 'm' Toggle on-off the measuring tool. - ========== ============================================ - - :param event: Ignored. - :return: None - """ - - if event.key == self.defaults['fit_key']: # 1 - self.on_zoom_fit(None) - return - - if event.key == self.defaults['zoom_out_key']: # 2 - self.plotcanvas.zoom(1 / self.defaults['zoom_ratio'], self.mouse) - return - - if event.key == self.defaults['zoom_in_key']: # 3 - self.plotcanvas.zoom(self.defaults['zoom_ratio'], self.mouse) - return - - # if event.key == 'm': - # if self.measure.toggle_active(): - # self.inform.emit("Measuring tool ON") - # else: - # self.inform.emit("Measuring tool OFF") - # return - - def on_click_over_plot(self, event): - """ - Callback for the mouse click event over the plot. This event is generated - by the Matplotlib backend and has been registered in ``self.__init__()``. - For details, see: http://matplotlib.org/users/event_handling.html - - Default actions are: - - * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679) - - :param event: Contains information about the event, like which button - was clicked, the pixel coordinates and the axes coordinates. - :return: None - """ - - # So it can receive key presses - self.plotcanvas.canvas.setFocus() - - try: - App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % ( - event.button, event.x, event.y, event.xdata, event.ydata)) - modifiers = QtGui.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ControlModifier: - self.clipboard.setText(self.defaults["point_clipboard_format"] % (event.xdata, event.ydata)) - - except Exception as e: - App.log.debug("Outside plot?") - App.log.debug(str(e)) - - def on_mouse_move_over_plot(self, event): - """ - Callback for the mouse motion event over the plot. This event is generated - by the Matplotlib backend and has been registered in ``self.__init__()``. - For details, see: http://matplotlib.org/users/event_handling.html - - :param event: Contains information about the event. - :return: None - """ - - try: # May fail in case mouse not within axes - self.ui.position_label.setText("X: %.4f Y: %.4f" % ( - event.xdata, event.ydata)) - self.mouse = [event.xdata, event.ydata] - - except: - self.ui.position_label.setText("") - self.mouse = None - - def on_file_new(self): - """ - Callback for menu item File->New. Returns the application to its - startup state. This method is thread-safe. - - :return: None - """ - - self.report_usage("on_file_new") - - # Remove everything from memory - App.log.debug("on_file_new()") - - 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() - - # Clear project filename - self.project_filename = None - - # Re-fresh project options - self.on_options_app2project() - - def on_fileopengerber(self): - """ - File menu callback for opening a Gerber. - - :return: None - """ - - self.report_usage("on_fileopengerber") - App.log.debug("on_fileopengerber()") - - try: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Gerber", - directory=self.get_last_folder()) - except TypeError: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Gerber") - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - - if filename == "": - self.inform.emit("Open cancelled.") - else: - self.worker_task.emit({'fcn': self.open_gerber, - 'params': [filename]}) - - def on_fileopenexcellon(self): - """ - File menu callback for opening an Excellon file. - - :return: None - """ - - self.report_usage("on_fileopenexcellon") - App.log.debug("on_fileopenexcellon()") - - try: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Excellon", - directory=self.get_last_folder()) - except TypeError: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Excellon") - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - - if filename == "": - self.inform.emit("Open cancelled.") - else: - self.worker_task.emit({'fcn': self.open_excellon, - 'params': [filename]}) - - def on_fileopengcode(self): - """ - File menu call back for opening gcode. - - :return: None - """ - - self.report_usage("on_fileopengcode") - App.log.debug("on_fileopengcode()") - - try: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open G-Code", - directory=self.get_last_folder()) - except TypeError: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open G-Code") - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - - if filename == "": - self.inform.emit("Open cancelled.") - else: - self.worker_task.emit({'fcn': self.open_gcode, - 'params': [filename]}) - - def on_file_openproject(self): - """ - File menu callback for opening a project. - - :return: None - """ - - self.report_usage("on_file_openproject") - App.log.debug("on_file_openproject()") - - try: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Project", - directory=self.get_last_folder()) - except TypeError: - filename = QtGui.QFileDialog.getOpenFileName(caption="Open Project") - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - - if filename == "": - self.inform.emit("Open cancelled.") - else: - # self.worker_task.emit({'fcn': self.open_project, - # 'params': [filename]}) - # The above was failing because open_project() is not - # 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 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 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 - ``self.project_filename`` or calls ``self.on_file_saveprojectas()`` - if set to None. The project is saved by calling ``self.save_project()``. - - :return: None - """ - - self.report_usage("on_file_saveproject") - - if self.project_filename is None: - self.on_file_saveprojectas() - else: - self.save_project(self.project_filename) - self.file_opened.emit("project", self.project_filename) - self.inform.emit("Project saved to: " + self.project_filename) - - def on_file_saveprojectas(self, make_copy=False): - """ - Callback for menu item File->Save Project As... Opens a file - chooser and saves the project to the given file via - ``self.save_project()``. - - :return: None - """ - - self.report_usage("on_file_saveprojectas") - - try: - filename = QtGui.QFileDialog.getSaveFileName(caption="Save Project As ...", - directory=self.get_last_folder()) - except TypeError: - filename = QtGui.QFileDialog.getSaveFileName(caption="Save Project As ...") - - filename = str(filename) - - try: - f = open(filename, 'r') - f.close() - exists = True - except IOError: - exists = False - - msg = "File exists. Overwrite?" - if exists: - msgbox = QtGui.QMessageBox() - msgbox.setInformativeText(msg) - msgbox.setStandardButtons(QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) - msgbox.setDefaultButton(QtGui.QMessageBox.Cancel) - result = msgbox.exec_() - if result == QtGui.QMessageBox.Cancel: - return - - self.save_project(filename) - self.file_opened.emit("project", filename) - - if not make_copy: - self.project_filename = filename - self.inform.emit("Project saved to: " + self.project_filename) - 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 - it in the program. Thread-safe. - - :param outname: Name of the resulting object. None causes the - name to be that of the file. - :param filename: Gerber file filename - :type filename: str - :param follow: If true, the parser will not create polygons, just lines - following the gerber path. - :type follow: bool - :return: None - """ - - # How the object should be initialized - def obj_init(gerber_obj, app_obj): - - assert isinstance(gerber_obj, FlatCAMGerber), \ - "Expected to initialize a FlatCAMGerber but got %s" % type(gerber_obj) - - # Opening the file happens here - self.progress.emit(30) - try: - gerber_obj.parse_file(filename, follow=follow) - - except IOError: - app_obj.inform.emit("[error] Failed to open file: " + filename) - app_obj.progress.emit(0) - raise IOError('Failed to open file: ' + filename) - - except ParseError as e: - app_obj.inform.emit("[error] Failed to parse file: " + filename + ". " + e[0]) - app_obj.progress.emit(0) - 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 - - App.log.debug("open_gerber()") - - with self.proc_container.new("Opening Gerber") as proc: - - self.progress.emit(10) - - # Object name - name = outname or filename.split('/')[-1].split('\\')[-1] - - ### Object creation ### - self.new_object("gerber", name, obj_init) - - # Register recent file - self.file_opened.emit("gerber", filename) - - self.progress.emit(100) - #proc.done() - - # GUI feedback - self.inform.emit("Opened: " + filename) - - def open_excellon(self, filename, outname=None): - """ - Opens an Excellon file, parses it and creates a new object for - it in the program. Thread-safe. - - :param outname: Name of the resulting object. None causes the - name to be that of the file. - :param filename: Excellon file filename - :type filename: str - :return: None - """ - - App.log.debug("open_excellon()") - - #self.progress.emit(10) - - # How the object should be initialized - def obj_init(excellon_obj, app_obj): - #self.progress.emit(20) - - 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: - 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."): - - # Object name - name = outname or filename.split('/')[-1].split('\\')[-1] - - self.new_object("excellon", name, obj_init) - - # Register recent file - self.file_opened.emit("excellon", filename) - - # GUI feedback - self.inform.emit("Opened: " + filename) - #self.progress.emit(100) - - def open_gcode(self, filename, outname=None): - """ - Opens a G-gcode file, parses it and creates a new object for - it in the program. Thread-safe. - - :param outname: Name of the resulting object. None causes the - name to be that of the file. - :param filename: G-code file filename - :type filename: str - :return: None - """ - App.log.debug("open_gcode()") - - # How the object should be initialized - def obj_init(job_obj, app_obj_): - """ - - :type app_obj_: App - """ - assert isinstance(app_obj_, App), \ - "Initializer expected App, got %s" % type(app_obj_) - - self.progress.emit(10) - - try: - f = open(filename) - gcode = f.read() - f.close() - except IOError: - app_obj_.inform.emit("[error] Failed to open " + filename) - self.progress.emit(0) - raise IOError("Failed to open " + filename) - - job_obj.gcode = gcode - - self.progress.emit(20) - job_obj.gcode_parse() - - self.progress.emit(60) - job_obj.create_geometry() - - with self.proc_container.new("Opening G-Code."): - - # Object name - name = outname or filename.split('/')[-1].split('\\')[-1] - - # New object creation and file processing - try: - self.new_object("cncjob", name, obj_init) - except Exception as e: - # e = sys.exc_info() - App.log.error(str(e)) - self.message_dialog("Failed to create CNCJob Object", - "Attempting to create a FlatCAM CNCJob Object from " + - "G-Code file failed during processing:\n" + - str(e[0]) + " " + str(e[1]), kind="error") - self.progress.emit(0) - self.collection.delete_active() - raise e - - # Register recent file - self.file_opened.emit("cncjob", filename) - - # GUI feedback - self.inform.emit("Opened: " + filename) - self.progress.emit(100) - - def open_project(self, filename): - """ - Loads a project from the specified file. - - 1) Loads and parses file - 2) Registers the file as recently opened. - 3) Calls on_file_new() - 4) Updates options - 5) Calls new_object() with the object's from_dict() as init method. - 6) Calls plot_all() - - :param filename: Name of the file from which to load. - :type filename: str - :return: None - """ - App.log.debug("Opening project: " + filename) - - ## Open and parse - try: - f = open(filename, 'r') - except IOError: - App.log.error("Failed to open project file: %s" % filename) - self.inform.emit("[error] Failed to open project file: %s" % filename) - return - - try: - d = json.load(f, object_hook=dict2obj) - except: - App.log.error("Failed to parse project file: %s" % filename) - self.inform.emit("[error] Failed to parse project file: %s" % filename) - f.close() - return - - self.file_opened.emit("project", filename) - - ## Clear the current project - ## NOT THREAD SAFE ## - self.on_file_new() - - ##Project options - self.options.update(d['options']) - self.project_filename = filename - #self.ui.units_label.setText("[" + self.options["units"] + "]") - self.set_screen_units(self.options["units"]) - - ## Re create objects - App.log.debug("Re-creating objects...") - for obj in d['objs']: - def obj_init(obj_inst, app_inst): - obj_inst.from_dict(obj) - App.log.debug(obj['kind'] + ": " + obj['options']['name']) - self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=False) - - self.plot_all() - self.inform.emit("Project loaded from: " + filename) - App.log.debug("Project loaded") - - def propagate_defaults(self): - """ - This method is used to set default values in classes. It's - an alternative to project options but allows the use - of values invisible to the user. - - :return: None - """ - - self.log.debug("propagate_defaults()") - - # Which objects to update the given parameters. - routes = { - "zdownrate": CNCjob, - "excellon_zeros": Excellon, - "gerber_use_buffer_for_union": Gerber, - "cncjob_coordinate_format": CNCjob - # "spindlespeed": CNCjob - } - - for param in routes: - if param in routes[param].defaults: - try: - routes[param].defaults[param] = self.defaults[param] - self.log.debug(" " + param + " OK") - except KeyError: - self.log.debug(" ERROR: " + param + " not in defaults.") - else: - # Try extracting the name: - # classname_param here is param in the object - if param.find(routes[param].__name__.lower() + "_") == 0: - p = param[len(routes[param].__name__) + 1:] - if p in routes[param].defaults: - routes[param].defaults[p] = self.defaults[param] - self.log.debug(" " + param + " OK!") - - def restore_main_win_geom(self): - self.ui.setGeometry(self.defaults["def_win_x"], - self.defaults["def_win_y"], - self.defaults["def_win_w"], - self.defaults["def_win_h"]) - - def plot_all(self): - """ - Re-generates all plots from all objects. - - :return: None - """ - self.log.debug("plot_all()") - - self.plotcanvas.clear() - self.progress.emit(10) - - def worker_task(app_obj): - percentage = 0.1 - try: - delta = 0.9 / len(self.collection.get_list()) - except ZeroDivisionError: - self.progress.emit(0) - return - for obj in self.collection.get_list(): - obj.plot() - percentage += delta - self.progress.emit(int(percentage*100)) - - self.progress.emit(0) - self.plots_updated.emit() - - # Send to worker - #self.worker.add_task(worker_task, [self]) - self.worker_task.emit({'fcn': worker_task, 'params': [self]}) - - def register_folder(self, filename): - self.defaults["last_folder"] = os.path.split(str(filename))[0] - - def set_progress_bar(self, percentage, text=""): - self.ui.progress_bar.setValue(int(percentage)) - - def setup_shell(self): - """ - Creates shell functions. Runs once at startup. - - :return: None - """ - - self.log.debug("setup_shell()") - - def shelp(p=None): - if not p: - 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: - return "Unknown command: %s" % p - - return commands[p]["help"] - - # --- Migrated to new architecture --- - # def options(name): - # ops = self.collection.get_by_name(str(name)).options - # return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops]) - - def h(*args): - """ - Pre-processes arguments to detect '-keyword value' pairs into dictionary - and standalone parameters into list. - """ - - kwa = {} - a = [] - 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: - a.append(args[i]) - else: - kwa[name] = args[i] - name = None - - 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" - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture - # def open_gerber(filename, *args): - # a, kwa = h(*args) - # types = {'follow': bool, - # 'outname': str} - # - # for key in kwa: - # if key not in types: - # return 'Unknown parameter: %s' % key - # kwa[key] = types[key](kwa[key]) - # - # self.open_gerber(str(filename), **kwa) - - # --- Migrated to new architecture --- - # def open_excellon(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.open_excellon(str(filename), **kwa) - - # --- Migrated to new architecture --- - # def open_gcode(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.open_gcode(str(filename), **kwa) - - # def cutout(name, *args): - # a, kwa = h(*args) - # types = {'dia': float, - # 'margin': float, - # 'gapsize': float, - # 'gaps': 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(name)) - # except: - # 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'] - # minx, miny, maxx, maxy = obj.bounds() - # minx -= margin - # maxx += margin - # miny -= margin - # maxy += margin - # midx = 0.5 * (minx + maxx) - # midy = 0.5 * (miny + maxy) - # hgap = 0.5 * gap_size - # pts = [[midx - hgap, maxy], - # [minx, maxy], - # [minx, midy + hgap], - # [minx, midy - hgap], - # [minx, miny], - # [midx - hgap, miny], - # [midx + hgap, miny], - # [maxx, miny], - # [maxx, midy - hgap], - # [maxx, midy + hgap], - # [maxx, maxy], - # [midx + hgap, maxy]] - # cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]], - # [pts[6], pts[7], pts[10], pts[11]]], - # "lr": [[pts[9], pts[10], pts[1], pts[2]], - # [pts[3], pts[4], pts[7], pts[8]]], - # "4": [[pts[0], pts[1], pts[2]], - # [pts[3], pts[4], pts[5]], - # [pts[6], pts[7], pts[8]], - # [pts[9], pts[10], pts[11]]]} - # cuts = cases[kwa['gaps']] - # geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) - # - # try: - # obj.app.new_object("geometry", name + "_cutout", geo_init_me) - # except Exception, e: - # return "Operation failed: %s" % str(e) - # - # return 'Ok' - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # def mirror(name, *args): - # a, kwa = h(*args) - # types = {'box': str, - # 'axis': str, - # '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: - # return "Could not retrieve object: %s" % name - # - # if obj is None: - # return "Object not found: %s" % name - # - # 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: - # axis = kwa['axis'].upper() - # except KeyError: - # return "ERROR: Specify -axis X or -axis Y" - # - # # Box - # if 'box' in kwa: - # try: - # box = self.collection.get_by_name(kwa['box']) - # except: - # return "Could not retrieve object box: %s" % kwa['box'] - # - # if box is None: - # return "Object box not found: %s" % kwa['box'] - # - # try: - # xmin, ymin, xmax, ymax = box.bounds() - # px = 0.5 * (xmin + xmax) - # py = 0.5 * (ymin + ymax) - # - # obj.mirror(axis, [px, py]) - # obj.plot() - # - # except Exception, e: - # return "Operation failed: %s" % str(e) - # - # else: - # try: - # dist = float(kwa['dist']) - # except KeyError: - # dist = 0.0 - # except ValueError: - # return "Invalid distance: %s" % kwa['dist'] - # - # try: - # obj.mirror(axis, [dist, dist]) - # obj.plot() - # except Exception, e: - # return "Operation failed: %s" % str(e) - # - # return 'Ok' - - # --- Migrated to new architecture --- - # def aligndrillgrid(outname, *args): - # a, kwa = h(*args) - # 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) - - # --- Migrated to new architecture --- - # 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: - # return "Could not retrieve object: %s" % name - # - # if obj is None: - # return "Object not found: %s" % name - # - # 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: - # 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 'box' not 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) - # - # 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) - # - # 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"}) - # - # init_obj.tools = tools - # init_obj.drills = drills - # init_obj.create_geometry() - # - # # Box - # if 'box' in kwa: - # try: - # box = self.collection.get_by_name(kwa['box']) - # except: - # return "Could not retrieve object box: %s" % kwa['box'] - # - # if box is None: - # return "Object box not found: %s" % kwa['box'] - # - # try: - # xmin, ymin, xmax, ymax = box.bounds() - # px = 0.5 * (xmin + xmax) - # py = 0.5 * (ymin + ymax) - # - # obj.app.new_object("excellon", name + "_aligndrill", alligndrill_init_me) - # - # except Exception, e: - # return "Operation failed: %s" % str(e) - # - # else: - # try: - # dist = float(kwa['dist']) - # except KeyError: - # dist = 0.0 - # except ValueError: - # return "Invalid distance: %s" % kwa['dist'] - # - # 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' - - # Migrated but still used? - # 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: - # a, kwa = h(*args) - # types = {'tools': str, - # 'outname': str, - # 'drillz': float, - # 'travelz': float, - # 'feedrate': float, - # 'spindlespeed': int, - # 'toolchange': int - # } - # - # 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 as 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) - # - # 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 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) - - # --- Migrated to new architecture --- - # 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: - # a, kwa = h(*args) - # types = {'tooldia': float, - # 'tools': str, - # '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: - # 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) - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # 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, - # 'combine': int} - # - # 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) - # - # 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: - # self.raise_tcl_error("Operation failed: %s" % str(e)) - # - # return 'Ok' - - # --- Migrated to new architecture --- - # def cncjob(obj_name, *args): - # a, kwa = h(*args) - # - # types = {'z_cut': float, - # 'z_move': float, - # 'feedrate': float, - # 'tooldia': float, - # 'outname': str, - # 'spindlespeed': int, - # 'multidepth' : bool, - # 'depthperpass' : float - # } - # - # 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 - # - # try: - # obj.generatecncjob(**kwa) - # except Exception, e: - # return "Operation failed: %s" % str(e) - # - # return 'Ok' - - # --- Migrated to new architecture --- - # def write_gcode(obj_name, filename, preamble='', postamble=''): - # """ - # Requires obj_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. - # """ - # - # # If there are promised objects, wait until all promises have been fulfilled. - # if self.collection.has_promises(): - # - # def write_gcode_on_object(new_object): - # self.log.debug("write_gcode_on_object(): Disconnecting %s" % write_gcode_on_object) - # self.new_object_available.disconnect(write_gcode_on_object) - # write_gcode(obj_name, filename, preamble, postamble) - # - # # Try again when a new object becomes available. - # self.log.debug("write_gcode(): Collection has promises. Queued for %s." % obj_name) - # self.log.debug("write_gcode(): Queued function: %s" % write_gcode_on_object) - # self.new_object_available.connect(write_gcode_on_object) - # - # return - # - # self.log.debug("write_gcode(): No promises. Continuing for %s." % obj_name) - # - # try: - # obj = self.collection.get_by_name(str(obj_name)) - # except: - # return "Could not retrieve object: %s" % obj_name - # - # try: - # obj.export_gcode(str(filename), str(preamble), str(postamble)) - # except Exception, e: - # return "Operation failed: %s" % str(e) - - # --- Migrated to new architecture --- - # def paint_poly(obj_name, inside_pt_x, inside_pt_y, tooldia, overlap): - # 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.paint_poly([float(inside_pt_x), float(inside_pt_y)], float(tooldia), float(overlap)) - - # --- New version in new geometry exists, but required here temporarily. --- - # def add_poly(obj_name, *args): - # """ - # Required by: add_rectangle() - # - # :param obj_name: - # :param args: - # :return: - # """ - # 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.add_polygon(points) - - # --- Migrated to new architecture --- - # def add_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y): - # return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, - # topright_x, topright_y, topright_x, botleft_y) - - # --- Migrated to new architecture --- - # def subtract_poly(obj_name, *args): - # """ - # Required by: subtract_rectangle() - # - # :param obj_name: - # :param args: - # :return: - # """ - # 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." - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # def add_circle(obj_name, center_x, center_y, radius): - # 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.add_circle([float(center_x), float(center_y)], float(radius)) - - # --- Migrated to new architecture --- - # def set_active(obj_name): - # try: - # self.collection.set_active(str(obj_name)) - # except Exception, e: - # return "Command failed: %s" % str(e) - - # --- Migrated to new architecture --- - # 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: - # return "Command failed: %s" % str(e) - - # --- Migrated to new architecture --- - # def geo_union(obj_name): - # - # 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.union() - - # --- Migrated to new architecture --- - # def join_geometries(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): - # FlatCAMGeometry.merge(objs, obj) - # - # if objs is not None: - # self.new_object("geometry", obj_name, initialize) - - # --- Migrated to new architecture --- - # 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) - - # --- Migrated to new architecture --- - # 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): - # self.new_object("excellon", local_outname, initialize_local_excellon) - # else: - # self.new_object("geometry", local_outname, initialize_local) - # - # currentx += lenghtx - # 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 - od = collections.OrderedDict(sorted(commands.items())) - for cmd_, val in list(od.items()): - #print cmd, '\n', ''.join(['~']*len(cmd)) - output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n' - - t = val['help'] - usage_i = t.find('>') - if usage_i < 0: - expl = t - #print expl + '\n' - output += expl + '\n\n' - continue - - expl = t[:usage_i - 1] - #print expl + '\n' - output += expl + '\n\n' - - end_usage_i = t[usage_i:].find('\n') - - if end_usage_i < 0: - end_usage_i = len(t[usage_i:]) - #print ' ' + t[usage_i:] - #print ' No parameters.\n' - output += ' ' + t[usage_i:] + '\n No parameters.\n' - else: - extras = t[usage_i+end_usage_i+1:] - parts = [s.strip() for s in extras.split('\n')] - - #print ' ' + t[usage_i:usage_i+end_usage_i] - output += ' ' + t[usage_i:usage_i+end_usage_i] + '\n' - for p in parts: - #print ' ' + p + '\n' - output += ' ' + p + '\n\n' - - return output - - - # def follow(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 - # - # try: - # obj.follow(**kwa) - # except Exception, e: - # return "ERROR: %s" % str(e) - - # def follow(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 - # - # try: - # obj.follow(**kwa) - # except Exception as e: - # return "ERROR: %s" % str(e) - - - # def get_sys(param): - # if param in self.defaults: - # return self.defaults[param] - # - # return "ERROR: No such system parameter." - - # def set_sys(param, value): - # # tcl string to python keywords: - # tcl2py = { - # "None": None, - # "none": None, - # "false": False, - # "False": False, - # "true": True, - # "True": True - # } - # - # if param in self.defaults: - # - # try: - # value = tcl2py[value] - # except KeyError: - # pass - # - # self.defaults[param] = value - # - # self.propagate_defaults() - # return "Ok" - # - # 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." - }, - # --- Migrated to new architecture --- - # 'import_svg': { - # 'fcn': import_svg, - # 'help': "Import an SVG file as a Geometry Object.\n" + - # "> import_svg " + - # " filename: Path to the file to import." - # }, - # --- Migrated to new architecture --- - # '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." - # }, - # --- Migrated to new architecture --- - # 'open_gerber': { - # 'fcn': open_gerber, - # '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." - # }, - # --- Migrated to new architecture --- - # 'open_excellon': { - # 'fcn': open_excellon, - # 'help': "Opens an Excellon file.\n" + - # "> open_excellon [-outname ]\n" + - # " filename: Path to file to open.\n" + - # " outname: Name of the created excellon object." - # }, - # --- Migrated to new architecture --- - # 'open_gcode': { - # 'fcn': open_gcode, - # 'help': "Opens an G-Code file.\n" + - # "> open_gcode [-outname ]\n" + - # " filename: Path to file to open.\n" + - # " outname: Name of the created CNC Job object." - # }, - # --- Migrated to new architecture --- - # 'open_project': { - # 'fcn': self.open_project, - # "help": "Opens a FlatCAM project.\n" + - # "> open_project \n" + - # " filename: Path to file to open." - # }, - # --- Migrated to new architecture --- - # 'save_project': { - # 'fcn': self.save_project, - # 'help': "Saves the FlatCAM project to file.\n" + - # "> save_project \n" + - # " filename: Path to file to save." - # }, - # --- Migrated to new architecture --- - # 'set_active': { - # 'fcn': set_active, - # 'help': "Sets a FlatCAM object as active.\n" + - # "> set_active \n" + - # " name: Name of the object." - # }, - # --- Migrated to new architecture --- - # 'get_names': { - # 'fcn': lambda: '\n'.join(self.collection.get_names()), - # 'help': "Lists the names of objects in the project.\n" + - # "> get_names" - # }, - # --- Migrated to new architecture --- - # 'new': { - # 'fcn': self.on_file_new, - # 'help': "Starts a new project. Clears objects from memory.\n" + - # "> new" - # }, - # --- Migrated to new architecture --- - # 'options': { - # 'fcn': options, - # 'help': "Shows the settings for an object.\n" + - # "> options \n" + - # " name: Object name." - # }, - # --- Migrated to new architecture --- - # 'isolate': { - # 'fcn': isolate, - # 'help': "Creates isolation routing geometry for the given Gerber.\n" + - # "> isolate [-dia ] [-passes

] [-overlap ] [-combine 0|1]\n" + - # " name: Name of the object.\n" - # " dia: Tool diameter\n passes: # of tool width.\n" + - # " overlap: Fraction of tool diameter to overlap passes." + - # " combine: combine all passes into one geometry." + - # " outname: Name of the resulting Geometry object." - # }, - # 'cutout': { - # 'fcn': cutout, - # 'help': "Creates board cutout.\n" + - # "> cutout [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps ]\n" + - # " name: Name of the object\n" + - # " dia: Tool diameter\n" + - # " margin: Margin over bounds\n" + - # " gapsize: size of gap\n" + - # " gaps: type of gaps" - # }, - # --- Migrated to new architecture --- - # '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" - # }, - # --- Migrated to new architecture --- - # 'mirror': { - # 'fcn': mirror, - # 'help': "Mirror a layer.\n" + - # "> mirror -axis [-box | -dist ]\n" + - # " name: Name of the object (Gerber or Excellon) to mirror.\n" + - # " box: Name of object which act as box (cutout for example.)\n" + - # " axis: Mirror axis parallel to the X or Y axis.\n" + - # " dist: Distance of the mirror axis to the X or Y axis." - #}, - # --- Migrated to new architecture --- - # '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" - # }, - # --- Migrated to new architecture --- - # '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." - # }, - # --- Migrated to new architecture --- - # 'exteriors': { - # 'fcn': exteriors, - # 'help': "Get exteriors of polygons.\n" + - # "> exteriors [-outname ]\n" + - # " name: Name of the source Geometry object.\n" + - # " outname: Name of the resulting Geometry object." - # }, - # --- Migrated to new architecture --- - # 'interiors': { - # 'fcn': interiors, - # 'help': "Get interiors of polygons.\n" + - # "> interiors [-outname ]\n" + - # " name: Name of the source Geometry object.\n" + - # " outname: Name of the resulting Geometry object." - # }, - # --- Migrated to new architecture --- - # 'drillcncjob': { - # 'fcn': drillcncjob, - # 'help': "Drill CNC job.\n" + - # "> drillcncjob -tools -drillz " + - # "-travelz -feedrate -outname " + - # "[-spindlespeed (int)] [-toolchange (int)] \n" + - # " name: Name of the object\n" + - # " tools: Comma separated indexes of tools (example: 1,3 or 2)\n" + - # " drillz: Drill depth into material (example: -2.0)\n" + - # " travelz: Travel distance above material (example: 2.0)\n" + - # " feedrate: Drilling feed rate\n" + - # " outname: Name of object to create\n" + - # " spindlespeed: Speed of the spindle in rpm (example: 4000)\n" + - # " toolchange: Enable tool changes (example: 1)\n" - # }, - # 'millholes': { - # 'fcn': millholes, - # 'help': "Create Geometry Object for milling holes from Excellon.\n" + - # "> millholes -tools -tooldia -outname \n" + - # " name: Name of the Excellon Object\n" + - # " tools: Comma separated indexes of tools (example: 1,3 or 2)\n" + - # " tooldia: Diameter of the milling tool (example: 0.1)\n" + - # " outname: Name of object to create\n" - # }, - # --- Migrated to the new architecture --- - # 'scale': { - # 'fcn': lambda name, factor: self.collection.get_by_name(str(name)).scale(float(factor)), - # 'help': "Resizes the object by a factor.\n" + - # "> scale \n" + - # " name: Name of the object\n factor: Fraction by which to scale" - # }, - # --- Migrated to the new architecture --- - # 'offset': { - # 'fcn': lambda name, x, y: self.collection.get_by_name(str(name)).offset([float(x), float(y)]), - # 'help': "Changes the position of the object.\n" + - # "> offset \n" + - # " name: Name of the object\n" + - # " x: X-axis distance\n" + - # " y: Y-axis distance" - # }, - # --- Migrated to new architecture --- - # 'plot': { - # 'fcn': self.plot_all, - # 'help': 'Updates the plot on the user interface' - # }, - # --- Migrated to new architecture --- - # 'cncjob': { - # 'fcn': cncjob, - # 'help': 'Generates a CNC Job from a Geometry Object.\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' - # }, - # --- Migrated to new architecture --- - # 'write_gcode': { - # 'fcn': write_gcode, - # 'help': 'Saves G-code of a CNC Job object to file.\n' + - # '> write_gcode \n' + - # ' name: Source CNC Job object\n' + - # ' filename: Output filename' - # }, - # --- Migrated to new architecture --- - # 'paint_poly': { - # 'fcn': paint_poly, - # 'help': 'Creates a geometry object with toolpath to cover the inside of a polygon.\n' + - # '> paint_poly \n' + - # ' name: Name of the sourge geometry object.\n' + - # ' inside_pt_x, inside_pt_y: Coordinates of a point inside the polygon.\n' + - # ' tooldia: Diameter of the tool to be used.\n' + - # ' overlap: Fraction of the tool diameter to overlap cuts.' - # }, - # --- Migrated to new architecture --- - # 'new_geometry': { - # 'fcn': lambda name: self.new_object('geometry', str(name), lambda x, y: None), - # 'help': 'Creates a new empty geometry object.\n' + - # '> new_geometry \n' + - # ' name: New object name' - # }, - # --- Migrated to new architecture --- - # 'add_poly': { - # 'fcn': add_poly, - # 'help': 'Creates a polygon in the given Geometry object.\n' + - # '> create_poly [x3 y3 [...]]\n' + - # ' name: Name of the geometry object to which to append the polygon.\n' + - # ' xi, yi: Coordinates of points in the polygon.' - # }, - # --- Migrated to new architecture --- - # '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.' - # }, - # --- Migrated to new architecture --- - # 'delete': { - # 'fcn': delete, - # 'help': 'Deletes the give object.\n' + - # '> delete \n' + - # ' name: Name of the object to delete.' - # }, - # --- Migrated to new architecture --- - # 'geo_union': { - # 'fcn': geo_union, - # 'help': 'Runs a union operation (addition) on the components ' + - # 'of the geometry object. For example, if it contains ' + - # '2 intersecting polygons, this opperation adds them into' + - # 'a single larger polygon.\n' + - # '> geo_union \n' + - # ' name: Name of the geometry object.' - # }, - # --- Migrated to new architecture --- - # 'join_geometries': { - # 'fcn': join_geometries, - # 'help': 'Runs a merge operation (join) on the geometry ' + - # 'objects.' + - # '> join_geometries ....\n' + - # ' out_name: Name of the new geometry object.' + - # ' obj_name_0... names of the objects to join' - # }, - # --- Migrated to new architecture --- - # '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' - # }, - # --- Migrated to new architecture --- - # '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.' - # }, - # --- Migrated to new architecture --- - # 'add_rect': { - # 'fcn': add_rectangle, - # 'help': 'Creates a rectange in the given Geometry object.\n' + - # '> add_rect \n' + - # ' name: Name of the geometry object to which to append the rectangle.\n' + - # ' botleft_x, botleft_y: Coordinates of the bottom left corner.\n' + - # ' topright_x, topright_y Coordinates of the top right corner.' - # }, - # --- Migrated to new architecture --- - # 'add_circle': { - # 'fcn': add_circle, - # 'help': 'Creates a circle in the given Geometry object.\n' + - # '> add_circle \n' + - # ' name: Name of the geometry object to which to append the circle.\n' + - # ' center_x, center_y: Coordinates of the center of the circle.\n' + - # ' radius: Radius of the circle.' - # }, - 'make_docs': { - 'fcn': make_docs, - 'help': 'Prints command rererence in reStructuredText format.' - }, - # 'follow': { - # 'fcn': follow, - # 'help': 'Creates a geometry object following gerber paths.\n' + - # '> follow [-outname ]\n' + - # ' name: Name of the gerber object.\n' + - # ' outname: Name of the output geometry object.' - # }, - - # 'get_sys': { - # 'fcn': get_sys, - # 'help': 'Get the value of a system parameter (FlatCAM constant)\n' + - # '> get_sys \n' + - # ' sysparam: Name of the parameter.' - # }, - # --- Migrated to new architecture --- - # 'set_sys': { - # 'fcn': set_sys, - # 'help': 'Set the value of a system parameter (FlatCAM constant)\n' + - # '> set_sys \n' + - # ' sysparam: Name of the parameter.\n' + - # ' paramvalue: Value to set.' - # } - } - - # Import/overwrite tcl commands as objects of TclCommand descendants - # This modifies the variable 'commands'. - tclCommands.register_all_commands(self, commands) - - # Add commands to the tcl interpreter - for cmd in commands: - self.tcl.createcommand(cmd, commands[cmd]['fcn']) - - # Make the tcl puts function return instead of print to stdout - self.tcl.eval(''' - rename puts original_puts - proc puts {args} { - if {[llength $args] == 1} { - return "[lindex $args 0]" - } else { - eval original_puts $args - } - } - ''') - - def setup_recent_items(self): - self.log.debug("setup_recent_items()") - - # TODO: Move this to constructor - icons = { - "gerber": "share/flatcam_icon16.png", - "excellon": "share/drill16.png", - "cncjob": "share/cnc16.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, - 'svg': self.import_svg - } - - # Open file - try: - f = open(self.data_path + '/recent.json') - except IOError: - App.log.error("Failed to load recent item list.") - self.inform.emit("[error] Failed to load recent item list.") - return - - try: - self.recent = json.load(f) - except json.scanner.JSONDecodeError: - App.log.error("Failed to parse recent item list.") - self.inform.emit("[error] Failed to parse recent item list.") - f.close() - return - f.close() - - # Closure needed to create callbacks in a loop. - # Otherwise late binding occurs. - def make_callback(func, fname): - def opener(): - func(fname) - return opener - - # Reset menu - self.ui.recent.clear() - - # Create menu items - for recent in self.recent: - filename = recent['filename'].split('/')[-1].split('\\')[-1] - - 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) - - 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) - # recent_menu.show_all() - # self.ui.recent.show() - - def setup_component_editor(self): - label = QtGui.QLabel("Choose an item from Project") - label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - self.ui.selected_scroll_area.setWidget(label) - - def setup_obj_classes(self): - """ - Sets up application specifics on the FlatCAMObj class. - - :return: None - """ - FlatCAMObj.app = self - - FCProcess.app = self - FCProcessContainer.app = self - - def version_check(self): - """ - Checks for the latest version of the program. Alerts the - user if theirs is outdated. This method is meant to be run - in a separate thread. - - :return: None - """ - - self.log.debug("version_check()") - full_url = App.version_url + \ - "?s=" + str(self.defaults['serial']) + \ - "&v=" + str(self.version) + \ - "&os=" + str(self.os) + \ - "&" + urllib.parse.urlencode(self.defaults["stats"]) - App.log.debug("Checking for updates @ %s" % full_url) - - ### Get the data - try: - f = urllib.request.urlopen(full_url) - except: - # App.log.warning("Failed checking for latest version. Could not connect.") - self.log.warning("Failed checking for latest version. Could not connect.") - self.inform.emit("[warning] Failed checking for latest version. Could not connect.") - return - - try: - data = json.load(f) - except Exception as e: - App.log.error("Could not parse information about latest version.") - self.inform.emit("[error] Could not parse information about latest version.") - App.log.debug("json.load(): %s" % str(e)) - f.close() - return - - f.close() - - ### Latest version? - if self.version >= data["version"]: - App.log.debug("FlatCAM is up to date!") - self.inform.emit("[success] FlatCAM is up to date!") - return - - App.log.debug("Newer version available.") - self.message.emit( - "Newer Version Available", - str("There is a newer version of FlatCAM " + - "available for download:

" + - "" + data["name"] + "
" + - data["message"].replace("\n", "
")), - "info" - ) - - def enable_all_plots(self, *args): - self.plotcanvas.clear() - - def worker_task(app_obj): - percentage = 0.1 - try: - delta = 0.9 / len(self.collection.get_list()) - except ZeroDivisionError: - self.progress.emit(0) - return - for obj in self.collection.get_list(): - obj.options['plot'] = True - obj.plot() - percentage += delta - self.progress.emit(int(percentage*100)) - - self.progress.emit(0) - self.plots_updated.emit() - - # Send to worker - # self.worker.add_task(worker_task, [self]) - self.worker_task.emit({'fcn': worker_task, 'params': [self]}) - - def save_project(self, filename): - """ - Saves the current project to the specified file. - - :param filename: Name of the file in which to save. - :type filename: str - :return: None - """ - self.log.debug("save_project()") - - ## Capture the latest changes - # Current object - try: - self.collection.get_active().read_form() - except: - self.log.debug("[warning] There was no active object") - pass - # Project options - self.options_read_form() - - # Serialize the whole project - d = {"objs": [obj.to_dict() for obj in self.collection.get_list()], - "options": self.options, - "version": self.version} - - # Open file - try: - f = open(filename, 'w') - except IOError: - App.log.error("[error] Failed to open file for saving: %s", filename) - return - - # Write - json.dump(d, f, default=to_dict, indent=2, sort_keys=True) - # try: - # json.dump(d, f, default=to_dict) - # except Exception, e: - # print str(e) - # App.log.error("[error] File open but failed to write: %s", filename) - # f.close() - # return - - f.close() - - self.inform.emit("Project saved to: %s" % filename) - -# def main(): -# -# app = QtGui.QApplication(sys.argv) -# fc = App() -# sys.exit(app.exec_()) -# -# -# if __name__ == '__main__': -# main() diff --git a/FlatCAMCommon.py b/FlatCAMCommon.py deleted file mode 100644 index 2cb9cefb..00000000 --- a/FlatCAMCommon.py +++ /dev/null @@ -1,48 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -class LoudDict(dict): - """ - A Dictionary with a callback for - item changes. - """ - - def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - self.callback = lambda x: None - - def __setitem__(self, key, value): - """ - Overridden __setitem__ method. Will emit 'changed(QString)' - if the item was changed, with key as parameter. - """ - if key in self and self.__getitem__(key) == value: - return - - dict.__setitem__(self, key, value) - self.callback(key) - - def update(self, *args, **kwargs): - if len(args) > 1: - raise TypeError("update expected at most 1 arguments, got %d" % len(args)) - other = dict(*args, **kwargs) - for key in other: - self[key] = other[key] - - def set_change_callback(self, callback): - """ - Assigns a function as callback on item change. The callback - will receive the key of the object that was changed. - - :param callback: Function to call on item change. - :type callback: func - :return: None - """ - - self.callback = callback - diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py deleted file mode 100644 index 6a3ccc3d..00000000 --- a/FlatCAMDraw.py +++ /dev/null @@ -1,1549 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from PyQt4 import QtGui, QtCore, Qt -import FlatCAMApp -from camlib import * -from FlatCAMTool import FlatCAMTool -from ObjectUI import LengthEntry, RadioSet - -from shapely.geometry import Polygon, LineString, Point, LinearRing -from shapely.geometry import MultiPoint, MultiPolygon -from shapely.geometry import box as shply_box -from shapely.ops import cascaded_union, unary_union -import shapely.affinity as affinity -from shapely.wkt import loads as sloads -from shapely.wkt import dumps as sdumps -from shapely.geometry.base import BaseGeometry - -from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, sign, dot -from numpy.linalg import solve - -#from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea - -from rtree import index as rtindex - -from GUIElements import FCEntry - - -class BufferSelectionTool(FlatCAMTool): - """ - Simple input for buffer distance. - """ - - toolName = "Buffer Selection" - - def __init__(self, app, fcdraw): - FlatCAMTool.__init__(self, app) - - self.fcdraw = fcdraw - - ## Title - title_label = QtGui.QLabel("%s" % self.toolName) - self.layout.addWidget(title_label) - - ## Form Layout - form_layout = QtGui.QFormLayout() - self.layout.addLayout(form_layout) - - ## Buffer distance - self.buffer_distance_entry = LengthEntry() - form_layout.addRow("Buffer distance:", self.buffer_distance_entry) - - ## Buttons - hlay = QtGui.QHBoxLayout() - self.layout.addLayout(hlay) - hlay.addStretch() - self.buffer_button = QtGui.QPushButton("Buffer") - hlay.addWidget(self.buffer_button) - - self.layout.addStretch() - - ## Signals - self.buffer_button.clicked.connect(self.on_buffer) - - def on_buffer(self): - buffer_distance = self.buffer_distance_entry.get_value() - self.fcdraw.buffer(buffer_distance) - - -class PaintOptionsTool(FlatCAMTool): - """ - Inputs to specify how to paint the selected polygons. - """ - - toolName = "Paint Options" - - def __init__(self, app, fcdraw): - FlatCAMTool.__init__(self, app) - - self.app = app - self.fcdraw = fcdraw - - ## Title - title_label = QtGui.QLabel("%s" % self.toolName) - self.layout.addWidget(title_label) - - ## Form Layout - form_layout = QtGui.QFormLayout() - self.layout.addLayout(form_layout) - - # Tool dia - ptdlabel = QtGui.QLabel('Tool dia:') - ptdlabel.setToolTip( - "Diameter of the tool to\n" - "be used in the operation." - ) - - self.painttooldia_entry = LengthEntry() - form_layout.addRow(ptdlabel, self.painttooldia_entry) - - # Overlap - ovlabel = QtGui.QLabel('Overlap:') - ovlabel.setToolTip( - "How much (fraction) of the tool\n" - "width to overlap each tool pass." - ) - - self.paintoverlap_entry = LengthEntry() - form_layout.addRow(ovlabel, self.paintoverlap_entry) - - # Margin - marginlabel = QtGui.QLabel('Margin:') - marginlabel.setToolTip( - "Distance by which to avoid\n" - "the edges of the polygon to\n" - "be painted." - ) - - self.paintmargin_entry = LengthEntry() - form_layout.addRow(marginlabel, self.paintmargin_entry) - - # Method - methodlabel = QtGui.QLabel('Method:') - methodlabel.setToolTip( - "Algorithm to paint the polygon:
" - "Standard: Fixed step inwards.
" - "Seed-based: Outwards from seed." - ) - - self.paintmethod_combo = RadioSet([ - {"label": "Standard", "value": "standard"}, - {"label": "Seed-based", "value": "seed"} - ]) - form_layout.addRow(methodlabel, self.paintmethod_combo) - - ## Buttons - hlay = QtGui.QHBoxLayout() - self.layout.addLayout(hlay) - hlay.addStretch() - self.paint_button = QtGui.QPushButton("Paint") - hlay.addWidget(self.paint_button) - - self.layout.addStretch() - - ## Signals - self.paint_button.clicked.connect(self.on_paint) - - def on_paint(self): - - tooldia = self.painttooldia_entry.get_value() - overlap = self.paintoverlap_entry.get_value() - margin = self.paintoverlap_entry.get_value() - method = self.paintmethod_combo.get_value() - - self.fcdraw.paint(tooldia, overlap, margin, method) - - -class DrawToolShape(object): - """ - Encapsulates "shapes" under a common class. - """ - - @staticmethod - def get_pts(o): - """ - Returns a list of all points in the object, where - the object can be a Polygon, Not a polygon, or a list - of such. Search is done recursively. - - :param: geometric object - :return: List of points - :rtype: list - """ - pts = [] - - ## Iterable: descend into each item. - try: - for subo in o: - pts += DrawToolShape.get_pts(subo) - - ## Non-iterable - except TypeError: - - ## DrawToolShape: descend into .geo. - if isinstance(o, DrawToolShape): - pts += DrawToolShape.get_pts(o.geo) - - ## Descend into .exerior and .interiors - elif type(o) == Polygon: - pts += DrawToolShape.get_pts(o.exterior) - for i in o.interiors: - pts += DrawToolShape.get_pts(i) - - ## Has .coords: list them. - else: - pts += list(o.coords) - - return pts - - def __init__(self, geo=[]): - - # Shapely type or list of such - self.geo = geo - self.utility = False - - def get_all_points(self): - return DrawToolShape.get_pts(self) - - -class DrawToolUtilityShape(DrawToolShape): - """ - Utility shapes are temporary geometry in the editor - to assist in the creation of shapes. For example it - will show the outline of a rectangle from the first - point to the current mouse pointer before the second - point is clicked and the final geometry is created. - """ - - def __init__(self, geo=[]): - super(DrawToolUtilityShape, self).__init__(geo=geo) - self.utility = True - - -class DrawTool(object): - """ - Abstract Class representing a tool in the drawing - program. Can generate geometry, including temporary - utility geometry that is updated on user clicks - and mouse motion. - """ - def __init__(self, draw_app): - self.draw_app = draw_app - self.complete = False - self.start_msg = "Click on 1st point..." - self.points = [] - self.geometry = None # DrawToolShape or None - - def click(self, point): - """ - :param point: [x, y] Coordinate pair. - """ - return "" - - def on_key(self, key): - return None - - def utility_geometry(self, data=None): - return None - - -class FCShapeTool(DrawTool): - """ - Abstarct class for tools that create a shape. - """ - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - - def make(self): - pass - - -class FCCircle(FCShapeTool): - """ - Resulting type: Polygon - """ - - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.start_msg = "Click on CENTER ..." - - def click(self, point): - self.points.append(point) - - if len(self.points) == 1: - return "Click on perimeter to complete ..." - - if len(self.points) == 2: - self.make() - return "Done." - - return "" - - def utility_geometry(self, data=None): - if len(self.points) == 1: - p1 = self.points[0] - p2 = data - radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) - return DrawToolUtilityShape(Point(p1).buffer(radius)) - - return None - - def make(self): - p1 = self.points[0] - p2 = self.points[1] - radius = distance(p1, p2) - self.geometry = DrawToolShape(Point(p1).buffer(radius)) - self.complete = True - - -class FCArc(FCShapeTool): - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.start_msg = "Click on CENTER ..." - - # Direction of rotation between point 1 and 2. - # 'cw' or 'ccw'. Switch direction by hitting the - # 'o' key. - self.direction = "cw" - - # Mode - # C12 = Center, p1, p2 - # 12C = p1, p2, Center - # 132 = p1, p3, p2 - self.mode = "c12" # Center, p1, p2 - - self.steps_per_circ = 55 - - def click(self, point): - self.points.append(point) - - if len(self.points) == 1: - return "Click on 1st point ..." - - if len(self.points) == 2: - return "Click on 2nd point to complete ..." - - if len(self.points) == 3: - self.make() - return "Done." - - return "" - - def on_key(self, key): - if key == 'o': - self.direction = 'cw' if self.direction == 'ccw' else 'ccw' - return 'Direction: ' + self.direction.upper() - - if key == 'p': - if self.mode == 'c12': - self.mode = '12c' - elif self.mode == '12c': - self.mode = '132' - else: - self.mode = 'c12' - return 'Mode: ' + self.mode - - def utility_geometry(self, data=None): - if len(self.points) == 1: # Show the radius - center = self.points[0] - p1 = data - - return DrawToolUtilityShape(LineString([center, p1])) - - if len(self.points) == 2: # Show the arc - - if self.mode == 'c12': - center = self.points[0] - p1 = self.points[1] - p2 = data - - radius = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2) - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p2[1] - center[1], p2[0] - center[0]) - - return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle, - self.direction, self.steps_per_circ)), - Point(center)]) - - elif self.mode == '132': - p1 = array(self.points[0]) - p3 = array(self.points[1]) - p2 = array(data) - - center, radius, t = three_point_circle(p1, p2, p3) - direction = 'cw' if sign(t) > 0 else 'ccw' - - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p3[1] - center[1], p3[0] - center[0]) - - return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle, - direction, self.steps_per_circ)), - Point(center), Point(p1), Point(p3)]) - - else: # '12c' - p1 = array(self.points[0]) - p2 = array(self.points[1]) - - # Midpoint - a = (p1 + p2) / 2.0 - - # Parallel vector - c = p2 - p1 - - # Perpendicular vector - b = dot(c, array([[0, -1], [1, 0]], dtype=float32)) - b /= norm(b) - - # Distance - t = distance(data, a) - - # Which side? Cross product with c. - # cross(M-A, B-A), where line is AB and M is test point. - side = (data[0] - p1[0]) * c[1] - (data[1] - p1[1]) * c[0] - t *= sign(side) - - # Center = a + bt - center = a + b * t - - radius = norm(center - p1) - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p2[1] - center[1], p2[0] - center[0]) - - return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle, - self.direction, self.steps_per_circ)), - Point(center)]) - - return None - - def make(self): - - if self.mode == 'c12': - center = self.points[0] - p1 = self.points[1] - p2 = self.points[2] - - radius = distance(center, p1) - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p2[1] - center[1], p2[0] - center[0]) - self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle, - self.direction, self.steps_per_circ))) - - elif self.mode == '132': - p1 = array(self.points[0]) - p3 = array(self.points[1]) - p2 = array(self.points[2]) - - center, radius, t = three_point_circle(p1, p2, p3) - direction = 'cw' if sign(t) > 0 else 'ccw' - - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p3[1] - center[1], p3[0] - center[0]) - - self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle, - direction, self.steps_per_circ))) - - else: # self.mode == '12c' - p1 = array(self.points[0]) - p2 = array(self.points[1]) - pc = array(self.points[2]) - - # Midpoint - a = (p1 + p2) / 2.0 - - # Parallel vector - c = p2 - p1 - - # Perpendicular vector - b = dot(c, array([[0, -1], [1, 0]], dtype=float32)) - b /= norm(b) - - # Distance - t = distance(pc, a) - - # Which side? Cross product with c. - # cross(M-A, B-A), where line is AB and M is test point. - side = (pc[0] - p1[0]) * c[1] - (pc[1] - p1[1]) * c[0] - t *= sign(side) - - # Center = a + bt - center = a + b * t - - radius = norm(center - p1) - startangle = arctan2(p1[1] - center[1], p1[0] - center[0]) - stopangle = arctan2(p2[1] - center[1], p2[0] - center[0]) - - self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle, - self.direction, self.steps_per_circ))) - self.complete = True - - -class FCRectangle(FCShapeTool): - """ - Resulting type: Polygon - """ - - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.start_msg = "Click on 1st corner ..." - - def click(self, point): - self.points.append(point) - - if len(self.points) == 1: - return "Click on opposite corner to complete ..." - - if len(self.points) == 2: - self.make() - return "Done." - - return "" - - def utility_geometry(self, data=None): - if len(self.points) == 1: - p1 = self.points[0] - p2 = data - return DrawToolUtilityShape(LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])) - - return None - - def make(self): - p1 = self.points[0] - p2 = self.points[1] - #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]) - self.geometry = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])) - self.complete = True - - -class FCPolygon(FCShapeTool): - """ - Resulting type: Polygon - """ - - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.start_msg = "Click on 1st point ..." - - def click(self, point): - self.points.append(point) - - if len(self.points) > 0: - return "Click on next point or hit SPACE to complete ..." - - return "" - - def utility_geometry(self, data=None): - if len(self.points) == 1: - temp_points = [x for x in self.points] - temp_points.append(data) - return DrawToolUtilityShape(LineString(temp_points)) - - if len(self.points) > 1: - temp_points = [x for x in self.points] - temp_points.append(data) - return DrawToolUtilityShape(LinearRing(temp_points)) - - return None - - def make(self): - # self.geometry = LinearRing(self.points) - self.geometry = DrawToolShape(Polygon(self.points)) - self.complete = True - - def on_key(self, key): - if key == 'backspace': - if len(self.points) > 0: - self.points = self.points[0:-1] - - -class FCPath(FCPolygon): - """ - Resulting type: LineString - """ - - def make(self): - self.geometry = DrawToolShape(LineString(self.points)) - self.complete = True - - def utility_geometry(self, data=None): - if len(self.points) > 0: - temp_points = [x for x in self.points] - temp_points.append(data) - return DrawToolUtilityShape(LineString(temp_points)) - - return None - - def on_key(self, key): - if key == 'backspace': - if len(self.points) > 0: - self.points = self.points[0:-1] - - -class FCSelect(DrawTool): - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.storage = self.draw_app.storage - #self.shape_buffer = self.draw_app.shape_buffer - self.selected = self.draw_app.selected - self.start_msg = "Click on geometry to select" - - def click(self, point): - try: - _, closest_shape = self.storage.nearest(point) - except StopIteration: - return "" - - if self.draw_app.key != 'control': - self.draw_app.selected = [] - - self.draw_app.set_selected(closest_shape) - self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo)) - - return "" - - -class FCMove(FCShapeTool): - def __init__(self, draw_app): - FCShapeTool.__init__(self, draw_app) - #self.shape_buffer = self.draw_app.shape_buffer - self.origin = None - self.destination = None - self.start_msg = "Click on reference point." - - def set_origin(self, origin): - self.origin = origin - - def click(self, point): - if len(self.draw_app.get_selected()) == 0: - return "Nothing to move." - - if self.origin is None: - self.set_origin(point) - return "Click on final location." - else: - self.destination = point - self.make() - return "Done." - - def make(self): - # Create new geometry - dx = self.destination[0] - self.origin[0] - dy = self.destination[1] - self.origin[1] - self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy)) - for geom in self.draw_app.get_selected()] - - # Delete old - self.draw_app.delete_selected() - - # # Select the new - # for g in self.geometry: - # # Note that g is not in the app's buffer yet! - # self.draw_app.set_selected(g) - - self.complete = True - - def utility_geometry(self, data=None): - """ - Temporary geometry on screen while using this tool. - - :param data: - :return: - """ - if self.origin is None: - return None - - if len(self.draw_app.get_selected()) == 0: - return None - - dx = data[0] - self.origin[0] - dy = data[1] - self.origin[1] - - return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy) - for geom in self.draw_app.get_selected()]) - - -class FCCopy(FCMove): - def make(self): - # Create new geometry - dx = self.destination[0] - self.origin[0] - dy = self.destination[1] - self.origin[1] - self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy)) - for geom in self.draw_app.get_selected()] - self.complete = True - - -######################## -### Main Application ### -######################## -class FlatCAMDraw(QtCore.QObject): - def __init__(self, app, disabled=False): - assert isinstance(app, FlatCAMApp.App), \ - "Expected the app to be a FlatCAMApp.App, got %s" % type(app) - - super(FlatCAMDraw, self).__init__() - - self.app = app - self.canvas = app.plotcanvas - self.axes = self.canvas.new_axes("draw") - - ### Drawing Toolbar ### - self.drawing_toolbar = QtGui.QToolBar("Draw Toolbar") - self.drawing_toolbar.setDisabled(disabled) - self.app.ui.addToolBar(self.drawing_toolbar) - - self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") - # Separator - self.drawing_toolbar.addSeparator() - self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') - self.add_arc_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') - self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle') - self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon') - self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path') - - # Separator - self.drawing_toolbar.addSeparator() - self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union') - self.intersection_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/intersection32.png'), 'Polygon Intersection') - self.subtract_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction') - self.cutpath_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') - - # Separator - self.drawing_toolbar.addSeparator() - self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") - self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") - self.delete_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") - - ### Snap Toolbar ### - - self.snap_toolbar = QtGui.QToolBar("Grid Toolbar") - self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid') - self.grid_gap_x_entry = FCEntry() - - self.grid_gap_x_entry.setMaximumWidth(70) - self.grid_gap_x_entry.setToolTip("Grid X distance") - self.snap_toolbar.addWidget(self.grid_gap_x_entry) - self.grid_gap_y_entry = FCEntry() - self.grid_gap_y_entry.setMaximumWidth(70) - self.grid_gap_y_entry.setToolTip("Grid Y distante") - self.snap_toolbar.addWidget(self.grid_gap_y_entry) - - - self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/corner32.png'), 'Snap to corner') - self.snap_max_dist_entry = FCEntry() - - self.snap_max_dist_entry.setMaximumWidth(70) - self.snap_max_dist_entry.setToolTip("Max. magnet distance") - self.snap_toolbar.addWidget(self.snap_max_dist_entry) - - self.snap_toolbar.setDisabled(disabled) - self.app.ui.addToolBar(self.snap_toolbar) - - ### Application menu ### - self.menu = QtGui.QMenu("Drawing") - self.app.ui.menu.insertMenu(self.app.ui.menutoolaction, self.menu) - # self.select_menuitem = self.menu.addAction(QtGui.QIcon('share:pointer16.png'), "Select 'Esc'") - # self.add_circle_menuitem = self.menu.addAction(QtGui.QIcon('share:circle16.png'), 'Add Circle') - # self.add_arc_menuitem = self.menu.addAction(QtGui.QIcon('share:arc16.png'), 'Add Arc') - # self.add_rectangle_menuitem = self.menu.addAction(QtGui.QIcon('share:rectangle16.png'), 'Add Rectangle') - # self.add_polygon_menuitem = self.menu.addAction(QtGui.QIcon('share:polygon16.png'), 'Add Polygon') - # self.add_path_menuitem = self.menu.addAction(QtGui.QIcon('share:path16.png'), 'Add Path') - self.union_menuitem = self.menu.addAction(QtGui.QIcon('share/union16.png'), 'Polygon Union') - self.intersection_menuitem = self.menu.addAction(QtGui.QIcon('share/intersection16.png'), 'Polygon Intersection') - # self.subtract_menuitem = self.menu.addAction(QtGui.QIcon('share:subtract16.png'), 'Polygon Subtraction') - self.cutpath_menuitem = self.menu.addAction(QtGui.QIcon('share/cutpath16.png'), 'Cut Path') - # Add Separator - self.menu.addSeparator() - # self.move_menuitem = self.menu.addAction(QtGui.QIcon('share:move16.png'), "Move Objects 'm'") - # self.copy_menuitem = self.menu.addAction(QtGui.QIcon('share:copy16.png'), "Copy Objects 'c'") - self.delete_menuitem = self.menu.addAction(QtGui.QIcon('share/deleteshape16.png'), "Delete Shape '-'") - self.buffer_menuitem = self.menu.addAction(QtGui.QIcon('share/buffer16.png'), "Buffer selection 'b'") - self.paint_menuitem = self.menu.addAction(QtGui.QIcon('share/paint16.png'), "Paint selection") - self.menu.addSeparator() - - self.paint_menuitem.triggered.connect(self.on_paint_tool) - self.buffer_menuitem.triggered.connect(self.on_buffer_tool) - self.delete_menuitem.triggered.connect(self.on_delete_btn) - self.union_menuitem.triggered.connect(self.union) - self.intersection_menuitem.triggered.connect(self.intersection) - self.cutpath_menuitem.triggered.connect(self.cutpath) - - ### Event handlers ### - # Connection ids for Matplotlib - self.cid_canvas_click = None - self.cid_canvas_move = None - self.cid_canvas_key = None - self.cid_canvas_key_release = None - - # Connect the canvas - #self.connect_canvas_event_handlers() - - self.union_btn.triggered.connect(self.union) - self.intersection_btn.triggered.connect(self.intersection) - self.subtract_btn.triggered.connect(self.subtract) - self.cutpath_btn.triggered.connect(self.cutpath) - self.delete_btn.triggered.connect(self.on_delete_btn) - - ## Toolbar events and properties - self.tools = { - "select": {"button": self.select_btn, - "constructor": FCSelect}, - "circle": {"button": self.add_circle_btn, - "constructor": FCCircle}, - "arc": {"button": self.add_arc_btn, - "constructor": FCArc}, - "rectangle": {"button": self.add_rectangle_btn, - "constructor": FCRectangle}, - "polygon": {"button": self.add_polygon_btn, - "constructor": FCPolygon}, - "path": {"button": self.add_path_btn, - "constructor": FCPath}, - "move": {"button": self.move_btn, - "constructor": FCMove}, - "copy": {"button": self.copy_btn, - "constructor": FCCopy} - } - - ### Data - self.active_tool = None - - self.storage = FlatCAMDraw.make_storage() - self.utility = [] - - ## List of selected shapes. - self.selected = [] - - self.move_timer = QtCore.QTimer() - self.move_timer.setSingleShot(True) - - self.key = None # Currently pressed key - - def make_callback(thetool): - def f(): - self.on_tool_select(thetool) - return f - - for tool in self.tools: - self.tools[tool]["button"].triggered.connect(make_callback(tool)) # Events - self.tools[tool]["button"].setCheckable(True) # Checkable - - # for snap_tool in [self.grid_snap_btn, self.corner_snap_btn]: - # snap_tool.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap")) - # snap_tool.setCheckable(True) - self.grid_snap_btn.setCheckable(True) - self.grid_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap")) - self.corner_snap_btn.setCheckable(True) - self.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap")) - - self.options = { - "snap-x": 0.1, - "snap-y": 0.1, - "snap_max": 0.05, - "grid_snap": False, - "corner_snap": False, - } - - self.grid_gap_x_entry.setText(str(self.options["snap-x"])) - self.grid_gap_y_entry.setText(str(self.options["snap-y"])) - self.snap_max_dist_entry.setText(str(self.options["snap_max"])) - - self.rtree_index = rtindex.Index() - - def entry2option(option, entry): - self.options[option] = float(entry.text()) - - self.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator()) - self.grid_gap_x_entry.editingFinished.connect(lambda: entry2option("snap-x", self.grid_gap_x_entry)) - self.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator()) - self.grid_gap_y_entry.editingFinished.connect(lambda: entry2option("snap-y", self.grid_gap_y_entry)) - self.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator()) - self.snap_max_dist_entry.editingFinished.connect(lambda: entry2option("snap_max", self.snap_max_dist_entry)) - - def activate(self): - pass - - def connect_canvas_event_handlers(self): - ## Canvas events - self.cid_canvas_click = self.canvas.mpl_connect('button_press_event', self.on_canvas_click) - self.cid_canvas_move = self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move) - self.cid_canvas_key = self.canvas.mpl_connect('key_press_event', self.on_canvas_key) - self.cid_canvas_key_release = self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release) - - def disconnect_canvas_event_handlers(self): - self.canvas.mpl_disconnect(self.cid_canvas_click) - self.canvas.mpl_disconnect(self.cid_canvas_move) - self.canvas.mpl_disconnect(self.cid_canvas_key) - self.canvas.mpl_disconnect(self.cid_canvas_key_release) - - def add_shape(self, shape): - """ - Adds a shape to the shape storage. - - :param shape: Shape to be added. - :type shape: DrawToolShape - :return: None - """ - - # List of DrawToolShape? - if isinstance(shape, list): - for subshape in shape: - self.add_shape(subshape) - return - - assert isinstance(shape, DrawToolShape), \ - "Expected a DrawToolShape, got %s" % type(shape) - - assert shape.geo is not None, \ - "Shape object has empty geometry (None)" - - assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \ - not isinstance(shape.geo, list), \ - "Shape objects has empty geometry ([])" - - if isinstance(shape, DrawToolUtilityShape): - self.utility.append(shape) - else: - self.storage.insert(shape) - - def deactivate(self): - self.disconnect_canvas_event_handlers() - self.clear() - self.drawing_toolbar.setDisabled(True) - self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool - - def delete_utility_geometry(self): - #for_deletion = [shape for shape in self.shape_buffer if shape.utility] - #for_deletion = [shape for shape in self.storage.get_objects() if shape.utility] - for_deletion = [shape for shape in self.utility] - for shape in for_deletion: - self.delete_shape(shape) - - def cutpath(self): - selected = self.get_selected() - tools = selected[1:] - toolgeo = cascaded_union([shp.geo for shp in tools]) - - target = selected[0] - if type(target.geo) == Polygon: - for ring in poly2rings(target.geo): - self.add_shape(DrawToolShape(ring.difference(toolgeo))) - self.delete_shape(target) - elif type(target.geo) == LineString or type(target.geo) == LinearRing: - self.add_shape(DrawToolShape(target.geo.difference(toolgeo))) - self.delete_shape(target) - else: - self.app.log.warning("Not implemented.") - - self.replot() - - def toolbar_tool_toggle(self, key): - self.options[key] = self.sender().isChecked() - - def clear(self): - self.active_tool = None - #self.shape_buffer = [] - self.selected = [] - self.storage = FlatCAMDraw.make_storage() - self.replot() - - def edit_fcgeometry(self, fcgeometry): - """ - Imports the geometry from the given FlatCAM Geometry object - into the editor. - - :param fcgeometry: FlatCAMGeometry - :return: None - """ - assert isinstance(fcgeometry, Geometry), \ - "Expected a Geometry, got %s" % type(fcgeometry) - - self.deactivate() - - self.connect_canvas_event_handlers() - self.select_tool("select") - - # Link shapes into editor. - for shape in fcgeometry.flatten(): - if shape is not None: # TODO: Make flatten never create a None - self.add_shape(DrawToolShape(shape)) - - self.replot() - self.drawing_toolbar.setDisabled(False) - self.snap_toolbar.setDisabled(False) - - def on_buffer_tool(self): - buff_tool = BufferSelectionTool(self.app, self) - buff_tool.run() - - def on_paint_tool(self): - paint_tool = PaintOptionsTool(self.app, self) - paint_tool.run() - - def on_tool_select(self, tool): - """ - Behavior of the toolbar. Tool initialization. - - :rtype : None - """ - self.app.log.debug("on_tool_select('%s')" % tool) - - # This is to make the group behave as radio group - if tool in self.tools: - if self.tools[tool]["button"].isChecked(): - self.app.log.debug("%s is checked." % tool) - for t in self.tools: - if t != tool: - self.tools[t]["button"].setChecked(False) - - self.active_tool = self.tools[tool]["constructor"](self) - self.app.inform.emit(self.active_tool.start_msg) - else: - self.app.log.debug("%s is NOT checked." % tool) - for t in self.tools: - self.tools[t]["button"].setChecked(False) - self.active_tool = None - - def on_canvas_click(self, event): - """ - event.x and .y have canvas coordinates - event.xdaya and .ydata have plot coordinates - - :param event: Event object dispatched by Matplotlib - :return: None - """ - # Selection with left mouse button - if self.active_tool is not None and event.button is 1: - # Dispatch event to active_tool - msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) - self.app.inform.emit(msg) - - # If it is a shape generating tool - if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete: - self.on_shape_complete() - return - - if isinstance(self.active_tool, FCSelect): - self.app.log.debug("Replotting after click.") - self.replot() - else: - self.app.log.debug("No active tool to respond to click!") - - def on_canvas_move(self, event): - """ - event.x and .y have canvas coordinates - event.xdaya and .ydata have plot coordinates - - :param event: Event object dispatched by Matplotlib - :return: - """ - self.on_canvas_move_effective(event) - return None - - # self.move_timer.stop() - # - # if self.active_tool is None: - # return - # - # # Make a function to avoid late evaluation - # def make_callback(): - # def f(): - # self.on_canvas_move_effective(event) - # return f - # callback = make_callback() - # - # self.move_timer.timeout.connect(callback) - # self.move_timer.start(500) # Stops if aready running - - def on_canvas_move_effective(self, event): - """ - Is called after timeout on timer set in on_canvas_move. - - For details on animating on MPL see: - http://wiki.scipy.org/Cookbook/Matplotlib/Animations - - event.x and .y have canvas coordinates - event.xdaya and .ydata have plot coordinates - - :param event: Event object dispatched by Matplotlib - :return: None - """ - - try: - x = float(event.xdata) - y = float(event.ydata) - except TypeError: - return - - if self.active_tool is None: - return - - ### Snap coordinates - x, y = self.snap(x, y) - - ### Utility geometry (animated) - self.canvas.canvas.restore_region(self.canvas.background) - geo = self.active_tool.utility_geometry(data=(x, y)) - - if isinstance(geo, DrawToolShape) and geo.geo is not None: - - # Remove any previous utility shape - self.delete_utility_geometry() - - # Add the new utility shape - self.add_shape(geo) - - # Efficient plotting for fast animation - - #self.canvas.canvas.restore_region(self.canvas.background) - elements = self.plot_shape(geometry=geo.geo, - linespec="b--", - linewidth=1, - animated=True) - for el in elements: - self.axes.draw_artist(el) - #self.canvas.canvas.blit(self.axes.bbox) - - # Pointer (snapped) - elements = self.axes.plot(x, y, 'bo', animated=True) - for el in elements: - self.axes.draw_artist(el) - - self.canvas.canvas.blit(self.axes.bbox) - - def on_canvas_key(self, event): - """ - event.key has the key. - - :param event: - :return: - """ - self.key = event.key - - ### Finish the current action. Use with tools that do not - ### complete automatically, like a polygon or path. - if event.key == ' ': - if isinstance(self.active_tool, FCShapeTool): - self.active_tool.click(self.snap(event.xdata, event.ydata)) - self.active_tool.make() - if self.active_tool.complete: - self.on_shape_complete() - self.app.inform.emit("Done.") - return - - ### Abort the current action - if event.key == 'escape': - # TODO: ...? - #self.on_tool_select("select") - self.app.inform.emit("Cancelled.") - - self.delete_utility_geometry() - - self.replot() - # self.select_btn.setChecked(True) - # self.on_tool_select('select') - self.select_tool('select') - return - - ### Delete selected object - if event.key == '-': - self.delete_selected() - self.replot() - - ### Move - if event.key == 'm': - self.move_btn.setChecked(True) - self.on_tool_select('move') - self.active_tool.set_origin(self.snap(event.xdata, event.ydata)) - self.app.inform.emit("Click on target point.") - - ### Copy - if event.key == 'c': - self.copy_btn.setChecked(True) - self.on_tool_select('copy') - self.active_tool.set_origin(self.snap(event.xdata, event.ydata)) - self.app.inform.emit("Click on target point.") - - ### Snap - if event.key == 'g': - self.grid_snap_btn.trigger() - if event.key == 'k': - self.corner_snap_btn.trigger() - - ### Buffer - if event.key == 'b': - self.on_buffer_tool() - - ### Propagate to tool - response = None - if self.active_tool is not None: - response = self.active_tool.on_key(event.key) - if response is not None: - self.app.inform.emit(response) - - def on_canvas_key_release(self, event): - self.key = None - - def on_delete_btn(self): - self.delete_selected() - self.replot() - - def get_selected(self): - """ - Returns list of shapes that are selected in the editor. - - :return: List of shapes. - """ - #return [shape for shape in self.shape_buffer if shape["selected"]] - return self.selected - - def delete_selected(self): - tempref = [s for s in self.selected] - for shape in tempref: - self.delete_shape(shape) - - self.selected = [] - - def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False): - """ - Plots a geometric object or list of objects without rendering. Plotted objects - are returned as a list. This allows for efficient/animated rendering. - - :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such) - :param linespec: Matplotlib linespec string. - :param linewidth: Width of lines in # of pixels. - :param animated: If geometry is to be animated. (See MPL plot()) - :return: List of plotted elements. - """ - plot_elements = [] - - if geometry is None: - geometry = self.active_tool.geometry - - try: - for geo in geometry: - plot_elements += self.plot_shape(geometry=geo, - linespec=linespec, - linewidth=linewidth, - animated=animated) - - ## Non-iterable - except TypeError: - - ## DrawToolShape - if isinstance(geometry, DrawToolShape): - plot_elements += self.plot_shape(geometry=geometry.geo, - linespec=linespec, - linewidth=linewidth, - animated=animated) - - ## Polygon: Dscend into exterior and each interior. - if type(geometry) == Polygon: - plot_elements += self.plot_shape(geometry=geometry.exterior, - linespec=linespec, - linewidth=linewidth, - animated=animated) - plot_elements += self.plot_shape(geometry=geometry.interiors, - linespec=linespec, - linewidth=linewidth, - animated=animated) - - if type(geometry) == LineString or type(geometry) == LinearRing: - x, y = geometry.coords.xy - element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated) - plot_elements.append(element) - - if type(geometry) == Point: - x, y = geometry.coords.xy - element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated) - plot_elements.append(element) - - return plot_elements - - def plot_all(self): - """ - Plots all shapes in the editor. - Clears the axes, plots, and call self.canvas.auto_adjust_axes. - - :return: None - :rtype: None - """ - self.app.log.debug("plot_all()") - self.axes.cla() - - for shape in self.storage.get_objects(): - if shape.geo is None: # TODO: This shouldn't have happened - continue - - if shape in self.selected: - self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2) - continue - - self.plot_shape(geometry=shape.geo) - - for shape in self.utility: - self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1) - continue - - self.canvas.auto_adjust_axes() - - def on_shape_complete(self): - self.app.log.debug("on_shape_complete()") - - # Add shape - self.add_shape(self.active_tool.geometry) - - # Remove any utility shapes - self.delete_utility_geometry() - - # Replot and reset tool. - self.replot() - self.active_tool = type(self.active_tool)(self) - - def delete_shape(self, shape): - - if shape in self.utility: - self.utility.remove(shape) - return - - self.storage.remove(shape) - - if shape in self.selected: - self.selected.remove(shape) - - def replot(self): - self.axes = self.canvas.new_axes("draw") - self.plot_all() - - @staticmethod - def make_storage(): - - ## Shape storage. - storage = FlatCAMRTreeStorage() - storage.get_points = DrawToolShape.get_pts - - return storage - - def select_tool(self, toolname): - """ - Selects a drawing tool. Impacts the object and GUI. - - :param toolname: Name of the tool. - :return: None - """ - self.tools[toolname]["button"].setChecked(True) - self.on_tool_select(toolname) - - def set_selected(self, shape): - - # Remove and add to the end. - if shape in self.selected: - self.selected.remove(shape) - - self.selected.append(shape) - - def set_unselected(self, shape): - if shape in self.selected: - self.selected.remove(shape) - - def snap(self, x, y): - """ - Adjusts coordinates to snap settings. - - :param x: Input coordinate X - :param y: Input coordinate Y - :return: Snapped (x, y) - """ - - snap_x, snap_y = (x, y) - snap_distance = Inf - - ### Object (corner?) snap - ### No need for the objects, just the coordinates - ### in the index. - if self.options["corner_snap"]: - try: - nearest_pt, shape = self.storage.nearest((x, y)) - - nearest_pt_distance = distance((x, y), nearest_pt) - if nearest_pt_distance <= self.options["snap_max"]: - snap_distance = nearest_pt_distance - snap_x, snap_y = nearest_pt - except (StopIteration, AssertionError): - pass - - ### Grid snap - if self.options["grid_snap"]: - if self.options["snap-x"] != 0: - snap_x_ = round(x / self.options["snap-x"]) * self.options['snap-x'] - else: - snap_x_ = x - - if self.options["snap-y"] != 0: - snap_y_ = round(y / self.options["snap-y"]) * self.options['snap-y'] - else: - snap_y_ = y - nearest_grid_distance = distance((x, y), (snap_x_, snap_y_)) - if nearest_grid_distance < snap_distance: - snap_x, snap_y = (snap_x_, snap_y_) - - return snap_x, snap_y - - def update_fcgeometry(self, fcgeometry): - """ - Transfers the drawing tool shape buffer to the selected geometry - object. The geometry already in the object are removed. - - :param fcgeometry: FlatCAMGeometry - :return: None - """ - fcgeometry.solid_geometry = [] - #for shape in self.shape_buffer: - for shape in self.storage.get_objects(): - fcgeometry.solid_geometry.append(shape.geo) - - def union(self): - """ - Makes union of selected polygons. Original polygons - are deleted. - - :return: None. - """ - - results = cascaded_union([t.geo for t in self.get_selected()]) - - # Delete originals. - for_deletion = [s for s in self.get_selected()] - for shape in for_deletion: - self.delete_shape(shape) - - # Selected geometry is now gone! - self.selected = [] - - self.add_shape(DrawToolShape(results)) - - self.replot() - - def intersection(self): - """ - Makes intersectino of selected polygons. Original polygons are deleted. - - :return: None - """ - - shapes = self.get_selected() - - results = shapes[0].geo - - for shape in shapes[1:]: - results = results.intersection(shape.geo) - - # Delete originals. - for_deletion = [s for s in self.get_selected()] - for shape in for_deletion: - self.delete_shape(shape) - - # Selected geometry is now gone! - self.selected = [] - - self.add_shape(DrawToolShape(results)) - - self.replot() - - def subtract(self): - selected = self.get_selected() - tools = selected[1:] - toolgeo = cascaded_union([shp.geo for shp in tools]) - result = selected[0].geo.difference(toolgeo) - - self.delete_shape(selected[0]) - self.add_shape(DrawToolShape(result)) - - self.replot() - - def buffer(self, buf_distance): - selected = self.get_selected() - - if len(selected) == 0: - self.app.inform.emit("[warning] Nothing selected for buffering.") - return - - if not isinstance(buf_distance, float): - self.app.inform.emit("[warning] Invalid distance for buffering.") - return - - pre_buffer = cascaded_union([t.geo for t in selected]) - results = pre_buffer.buffer(buf_distance) - self.add_shape(DrawToolShape(results)) - - self.replot() - - def paint(self, tooldia, overlap, margin, method): - selected = self.get_selected() - - if len(selected) == 0: - self.app.inform.emit("[warning] Nothing selected for painting.") - return - - for param in [tooldia, overlap, margin]: - if not isinstance(param, float): - param_name = [k for k, v in list(locals().items()) if v is param][0] - self.app.inform.emit("[warning] Invalid value for {}".format()) - - # Todo: Check for valid method. - - # Todo: This is the 3rd implementation on painting polys... try to consolidate - - results = [] - - def recurse(geo): - try: - for subg in geo: - for subsubg in recurse(subg): - yield subsubg - except TypeError: - if isinstance(geo, Polygon): - yield geo - - raise StopIteration - - for geo in selected: - - local_results = [] - for poly in recurse(geo.geo): - - if method == "seed": - # Type(cp) == FlatCAMRTreeStorage | None - cp = Geometry.clear_polygon2(poly.buffer(-margin), - tooldia, overlap=overlap) - - else: - # Type(cp) == FlatCAMRTreeStorage | None - cp = Geometry.clear_polygon(poly.buffer(-margin), - tooldia, overlap=overlap) - - if cp is not None: - local_results += list(cp.get_objects()) - - results.append(cascaded_union(local_results)) - - # This is a dirty patch: - for r in results: - self.add_shape(DrawToolShape(r)) - - self.replot() - - -def distance(pt1, pt2): - return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) - - -def mag(vec): - return sqrt(vec[0] ** 2 + vec[1] ** 2) - - -def poly2rings(poly): - return [poly.exterior] + [interior for interior in poly.interiors] \ No newline at end of file diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py deleted file mode 100644 index 0c3829d5..00000000 --- a/FlatCAMGUI.py +++ /dev/null @@ -1,979 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ -from PyQt4 import QtGui, QtCore, Qt -from GUIElements import * - - -class FlatCAMGUI(QtGui.QMainWindow): - - # Emitted when persistent window geometry needs to be retained - geom_update = QtCore.pyqtSignal(int, int, int, int, name='geomUpdate') - - def __init__(self, version, name=None): - super(FlatCAMGUI, self).__init__() - - # Divine icon pack by Ipapun @ finicons.com - - ############ - ### Menu ### - ############ - self.menu = self.menuBar() - - ### File ### - self.menufile = self.menu.addMenu('&File') - - # New - self.menufilenew = QtGui.QAction(QtGui.QIcon('share/file16.png'), '&New project', self) - self.menufile.addAction(self.menufilenew) - - # Recent - self.recent = self.menufile.addMenu(QtGui.QIcon('share/folder16.png'), "Open recent ...") - - # Separator - self.menufile.addSeparator() - - # 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) - - # Separator - self.menufile.addSeparator() - - # Import SVG ... - self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) - self.menufile.addAction(self.menufileimportsvg) - - # Export SVG ... - self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self) - self.menufile.addAction(self.menufileexportsvg) - - # Separator - self.menufile.addSeparator() - - # 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(QtGui.QIcon('share/floppy16.png'), 'Save Project &As ...', self) - self.menufile.addAction(self.menufilesaveprojectas) - - # Save Project Copy ... - self.menufilesaveprojectcopy = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project C&opy ...', self) - self.menufile.addAction(self.menufilesaveprojectcopy) - - # Save Defaults - self.menufilesavedefaults = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save &Defaults', self) - self.menufile.addAction(self.menufilesavedefaults) - - # Separator - self.menufile.addSeparator() - - # Quit - self.exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), '&Exit', self) - self.menufile.addAction(self.exit_action) - # exitAction.setShortcut('Ctrl+Q') - # exitAction.setStatusTip('Exit application') - #self.exit_action.triggered.connect(QtGui.qApp.quit) - - ### Edit ### - self.menuedit = self.menu.addMenu('&Edit') - self.menueditnew = self.menuedit.addAction(QtGui.QIcon('share/new_geo16.png'), 'New Geometry') - self.menueditedit = self.menuedit.addAction(QtGui.QIcon('share/edit16.png'), 'Edit Geometry') - self.menueditok = self.menuedit.addAction(QtGui.QIcon('share/edit_ok16.png'), 'Update Geometry') - # Separator - self.menuedit.addSeparator() - self.menueditjoin = self.menuedit.addAction(QtGui.QIcon('share/join16.png'), 'Join Geometry') - self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), 'Delete') - self.menuedit.addSeparator() - - - ### Options ### - self.menuoptions = self.menu.addMenu('&Options') - self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options') - self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project") - self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application") - self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object") - self.menuoptions_transfer_o2p = self.menuoptions_transfer.addAction("Object to Project") - self.menuoptions_transfer_a2o = self.menuoptions_transfer.addAction("Application to Object") - self.menuoptions_transfer_o2a = self.menuoptions_transfer.addAction("Object to Application") - - ### View ### - self.menuview = self.menu.addMenu('&View') - self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'), 'Disable all plots') - self.menuviewdisableother = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'), - 'Disable all plots but this one') - self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots') - - ### Tool ### - - self.menutool = QtGui.QMenu('&Tool') - self.menutoolaction = self.menu.addMenu(self.menutool) - self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line') - - ### Help ### - self.menuhelp = self.menu.addMenu('&Help') - self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/tv16.png'), 'About FlatCAM') - self.menuhelp_home = self.menuhelp.addAction(QtGui.QIcon('share/home16.png'), 'Home') - self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), 'Manual') - - ############### - ### Toolbar ### - ############### - self.toolbarfile = QtGui.QToolBar('File Toolbar') - self.addToolBar(self.toolbarfile) - self.open_gerber_btn = self.toolbarfile.addAction(QtGui.QIcon('share/flatcam_icon32.png'), "Open &Gerber") - self.open_exc_btn = self.toolbarfile.addAction(QtGui.QIcon('share/drill32.png'), "Open &Excellon") - self.open_gcode_btn = self.toolbarfile.addAction(QtGui.QIcon('share/cnc32.png'), "Open Gco&de") - self.save_btn = self.toolbarfile.addAction(QtGui.QIcon('share/floppy32.png'), 'Save Project &As ...') - - self.toolbarview= QtGui.QToolBar('View Toolbar') - self.addToolBar(self.toolbarview) - self.zoom_fit_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "&Zoom Fit") - self.zoom_out_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_out32.png'), "&Zoom Out") - self.zoom_in_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In") - # Separator - self.toolbarview.addSeparator() - self.clear_plot_btn = self.toolbarview.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear Plot") - self.replot_btn = self.toolbarview.addAction(QtGui.QIcon('share/replot32.png'), "&Replot") - - self.toolbareditobj = QtGui.QToolBar('Obj.Editor Toolbar') - self.addToolBar(self.toolbareditobj) - self.newgeo_btn = self.toolbareditobj.addAction(QtGui.QIcon('share/new_geo32.png'), "New Blank Geometry") - self.editgeo_btn = self.toolbareditobj.addAction(QtGui.QIcon('share/edit32.png'), "Edit Geometry") - self.updategeo_btn = self.toolbareditobj.addAction(QtGui.QIcon('share/edit_ok32.png'), "Update Geometry") - self.updategeo_btn.setEnabled(False) - - self.toolbaredit = QtGui.QToolBar('Edit Toolbar') - self.addToolBar(self.toolbaredit) - self.delete_btn = self.toolbaredit.addAction(QtGui.QIcon('share/delete32.png'), "&Delete") - - self.toolbartools = QtGui.QToolBar('Tools Toolbar') - self.addToolBar(self.toolbartools) - self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") - self.measure_btn = self.toolbartools.addAction(QtGui.QIcon('share/measure32.png'), "&Measurement Tool") - - ################ - ### Splitter ### - ################ - self.splitter = QtGui.QSplitter() - self.setCentralWidget(self.splitter) - - ################ - ### Notebook ### - ################ - self.notebook = QtGui.QTabWidget() - - # self.notebook.setMinimumWidth(250) - - ### Project ### - 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") - - ### Selected ### - self.selected_tab = QtGui.QWidget() - self.selected_tab_layout = QtGui.QVBoxLayout(self.selected_tab) - self.selected_tab_layout.setContentsMargins(2, 2, 2, 2) - self.selected_scroll_area = VerticalScrollArea() - self.selected_tab_layout.addWidget(self.selected_scroll_area) - self.notebook.addTab(self.selected_tab, "Selected") - - ### Options ### - self.options_tab = QtGui.QWidget() - self.options_tab.setContentsMargins(0, 0, 0, 0) - self.options_tab_layout = QtGui.QVBoxLayout(self.options_tab) - self.options_tab_layout.setContentsMargins(2, 2, 2, 2) - - hlay1 = QtGui.QHBoxLayout() - self.options_tab_layout.addLayout(hlay1) - - self.icon = QtGui.QLabel() - self.icon.setPixmap(QtGui.QPixmap('share/gear48.png')) - hlay1.addWidget(self.icon) - - self.options_combo = QtGui.QComboBox() - self.options_combo.addItem("APPLICATION DEFAULTS") - self.options_combo.addItem("PROJECT OPTIONS") - hlay1.addWidget(self.options_combo) - hlay1.addStretch() - - self.options_scroll_area = VerticalScrollArea() - self.options_tab_layout.addWidget(self.options_scroll_area) - - self.notebook.addTab(self.options_tab, "Options") - - ### Tool ### - self.tool_tab = QtGui.QWidget() - self.tool_tab_layout = QtGui.QVBoxLayout(self.tool_tab) - self.tool_tab_layout.setContentsMargins(2, 2, 2, 2) - self.notebook.addTab(self.tool_tab, "Tool") - self.tool_scroll_area = VerticalScrollArea() - self.tool_tab_layout.addWidget(self.tool_scroll_area) - - self.splitter.addWidget(self.notebook) - - ###################### - ### Plot and other ### - ###################### - right_widget = QtGui.QWidget() - # right_widget.setContentsMargins(0, 0, 0, 0) - self.splitter.addWidget(right_widget) - self.right_layout = QtGui.QVBoxLayout() - self.right_layout.setMargin(0) - # self.right_layout.setContentsMargins(0, 0, 0, 0) - right_widget.setLayout(self.right_layout) - - ################ - ### Info bar ### - ################ - infobar = self.statusBar() - - #self.info_label = QtGui.QLabel("Welcome to FlatCAM.") - #self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) - #infobar.addWidget(self.info_label, stretch=1) - self.fcinfo = FlatCAMInfoBar() - infobar.addWidget(self.fcinfo, stretch=1) - - self.position_label = QtGui.QLabel("") - #self.position_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) - self.position_label.setMinimumWidth(110) - infobar.addWidget(self.position_label) - - self.units_label = QtGui.QLabel("[in]") - # self.units_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) - self.units_label.setMargin(2) - infobar.addWidget(self.units_label) - - self.progress_bar = QtGui.QProgressBar() - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(100) - #infobar.addWidget(self.progress_bar) - - self.activity_view = FlatCAMActivityView() - infobar.addWidget(self.activity_view) - - ############# - ### Icons ### - ############# - self.app_icon = QtGui.QIcon() - self.app_icon.addFile('share/flatcam_icon16.png', QtCore.QSize(16, 16)) - self.app_icon.addFile('share/flatcam_icon24.png', QtCore.QSize(24, 24)) - self.app_icon.addFile('share/flatcam_icon32.png', QtCore.QSize(32, 32)) - self.app_icon.addFile('share/flatcam_icon48.png', QtCore.QSize(48, 48)) - self.app_icon.addFile('share/flatcam_icon128.png', QtCore.QSize(128, 128)) - self.app_icon.addFile('share/flatcam_icon256.png', QtCore.QSize(256, 256)) - self.setWindowIcon(self.app_icon) - - self.setGeometry(100, 100, 1024, 650) - title = 'FlatCAM {}'.format(version) - if name is not None: - title += ' - {}'.format(name) - self.setWindowTitle(title) - self.show() - - def closeEvent(self, event): - grect = self.geometry() - self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height()) - QtGui.qApp.quit() - - - -class FlatCAMActivityView(QtGui.QWidget): - - def __init__(self, parent=None): - super(FlatCAMActivityView, self).__init__(parent=parent) - - self.setMinimumWidth(200) - - self.icon = QtGui.QLabel(self) - self.icon.setGeometry(0, 0, 12, 12) - self.movie = QtGui.QMovie("share/active.gif") - self.icon.setMovie(self.movie) - #self.movie.start() - - layout = QtGui.QHBoxLayout() - layout.setContentsMargins(5, 0, 5, 0) - layout.setAlignment(QtCore.Qt.AlignLeft) - self.setLayout(layout) - - layout.addWidget(self.icon) - self.text = QtGui.QLabel(self) - self.text.setText("Idle.") - - layout.addWidget(self.text) - - def set_idle(self): - self.movie.stop() - self.text.setText("Idle.") - - def set_busy(self, msg): - self.movie.start() - self.text.setText(msg) - - -class FlatCAMInfoBar(QtGui.QWidget): - - def __init__(self, parent=None): - super(FlatCAMInfoBar, self).__init__(parent=parent) - - self.icon = QtGui.QLabel(self) - self.icon.setGeometry(0, 0, 12, 12) - self.pmap = QtGui.QPixmap('share/graylight12.png') - self.icon.setPixmap(self.pmap) - - layout = QtGui.QHBoxLayout() - layout.setContentsMargins(5, 0, 5, 0) - self.setLayout(layout) - - layout.addWidget(self.icon) - - self.text = QtGui.QLabel(self) - self.text.setText("Hello!") - self.text.setToolTip("Hello!") - - layout.addWidget(self.text) - - layout.addStretch() - - def set_text_(self, text): - self.text.setText(text) - self.text.setToolTip(text) - - def set_status(self, text, level="info"): - level = str(level) - self.pmap.fill() - if level == "error": - self.pmap = QtGui.QPixmap('share/redlight12.png') - elif level == "success": - self.pmap = QtGui.QPixmap('share/greenlight12.png') - elif level == "warning": - self.pmap = QtGui.QPixmap('share/yellowlight12.png') - else: - self.pmap = QtGui.QPixmap('share/graylight12.png') - - self.icon.setPixmap(self.pmap) - self.set_text_(text) - - -class OptionsGroupUI(QtGui.QGroupBox): - def __init__(self, title, parent=None): - QtGui.QGroupBox.__init__(self, title, parent=parent) - self.setStyleSheet(""" - QGroupBox - { - font-size: 16px; - font-weight: bold; - } - """) - - self.layout = QtGui.QVBoxLayout() - self.setLayout(self.layout) - - -class GerberOptionsGroupUI(OptionsGroupUI): - def __init__(self, parent=None): - OptionsGroupUI.__init__(self, "Gerber Options", parent=parent) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.layout.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.layout.addLayout(grid0) - # Plot CB - self.plot_cb = FCCheckBox(label='Plot') - self.plot_options_label.setToolTip( - "Plot (show) this object." - ) - - grid0.addWidget(self.plot_cb, 0, 0) - - # Solid CB - self.solid_cb = FCCheckBox(label='Solid') - self.solid_cb.setToolTip( - "Solid color polygons." - ) - grid0.addWidget(self.solid_cb, 0, 1) - - # Multicolored CB - self.multicolored_cb = FCCheckBox(label='Multicolored') - self.multicolored_cb.setToolTip( - "Draw polygons in different colors." - ) - grid0.addWidget(self.multicolored_cb, 0, 2) - - ## Isolation Routing - self.isolation_routing_label = QtGui.QLabel("Isolation Routing:") - self.isolation_routing_label.setToolTip( - "Create a Geometry object with\n" - "toolpaths to cut outside polygons." - ) - self.layout.addWidget(self.isolation_routing_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.iso_tool_dia_entry = LengthEntry() - grid1.addWidget(self.iso_tool_dia_entry, 0, 1) - - passlabel = QtGui.QLabel('Width (# passes):') - passlabel.setToolTip( - "Width of the isolation gap in\n" - "number (integer) of tool widths." - ) - grid1.addWidget(passlabel, 1, 0) - self.iso_width_entry = IntEntry() - grid1.addWidget(self.iso_width_entry, 1, 1) - - overlabel = QtGui.QLabel('Pass overlap:') - overlabel.setToolTip( - "How much (fraction of tool width)\n" - "to overlap each pass." - ) - grid1.addWidget(overlabel, 2, 0) - self.iso_overlap_entry = FloatEntry() - grid1.addWidget(self.iso_overlap_entry, 2, 1) - - self.combine_passes_cb = FCCheckBox(label='Combine Passes') - self.combine_passes_cb.setToolTip( - "Combine all passes into one object" - ) - grid1.addWidget(self.combine_passes_cb, 3, 0) - - ## Board cuttout - self.board_cutout_label = QtGui.QLabel("Board cutout:") - self.board_cutout_label.setToolTip( - "Create toolpaths to cut around\n" - "the PCB and separate it from\n" - "the original board." - ) - self.layout.addWidget(self.board_cutout_label) - - grid2 = QtGui.QGridLayout() - self.layout.addLayout(grid2) - tdclabel = QtGui.QLabel('Tool dia:') - tdclabel.setToolTip( - "Diameter of the cutting tool." - ) - grid2.addWidget(tdclabel, 0, 0) - self.cutout_tooldia_entry = LengthEntry() - grid2.addWidget(self.cutout_tooldia_entry, 0, 1) - - marginlabel = QtGui.QLabel('Margin:') - marginlabel.setToolTip( - "Distance from objects at which\n" - "to draw the cutout." - ) - grid2.addWidget(marginlabel, 1, 0) - self.cutout_margin_entry = LengthEntry() - grid2.addWidget(self.cutout_margin_entry, 1, 1) - - gaplabel = QtGui.QLabel('Gap size:') - gaplabel.setToolTip( - "Size of the gaps in the toolpath\n" - "that will remain to hold the\n" - "board in place." - ) - grid2.addWidget(gaplabel, 2, 0) - self.cutout_gap_entry = LengthEntry() - grid2.addWidget(self.cutout_gap_entry, 2, 1) - - gapslabel = QtGui.QLabel('Gaps:') - gapslabel.setToolTip( - "Where to place the gaps, Top/Bottom\n" - "Left/Rigt, or on all 4 sides." - ) - grid2.addWidget(gapslabel, 3, 0) - self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'}, - {'label': '2 (L/R)', 'value': 'lr'}, - {'label': '4', 'value': '4'}]) - grid2.addWidget(self.gaps_radio, 3, 1) - - ## Non-copper regions - self.noncopper_label = QtGui.QLabel("Non-copper regions:") - self.noncopper_label.setToolTip( - "Create polygons covering the\n" - "areas without copper on the PCB.\n" - "Equivalent to the inverse of this\n" - "object. Can be used to remove all\n" - "copper from a specified region." - ) - self.layout.addWidget(self.noncopper_label) - - grid3 = QtGui.QGridLayout() - self.layout.addLayout(grid3) - - # Margin - bmlabel = QtGui.QLabel('Boundary Margin:') - bmlabel.setToolTip( - "Specify the edge of the PCB\n" - "by drawing a box around all\n" - "objects with this minimum\n" - "distance." - ) - grid3.addWidget(bmlabel, 0, 0) - self.noncopper_margin_entry = LengthEntry() - grid3.addWidget(self.noncopper_margin_entry, 0, 1) - - # Rounded corners - self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners") - self.noncopper_rounded_cb.setToolTip( - "Creates a Geometry objects with polygons\n" - "covering the copper-free areas of the PCB." - ) - grid3.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2) - - ## Bounding box - self.boundingbox_label = QtGui.QLabel('Bounding Box:') - self.layout.addWidget(self.boundingbox_label) - - grid4 = QtGui.QGridLayout() - self.layout.addLayout(grid4) - - bbmargin = QtGui.QLabel('Boundary Margin:') - bbmargin.setToolTip( - "Distance of the edges of the box\n" - "to the nearest polygon." - ) - grid4.addWidget(bbmargin, 0, 0) - self.bbmargin_entry = LengthEntry() - grid4.addWidget(self.bbmargin_entry, 0, 1) - - self.bbrounded_cb = FCCheckBox(label="Rounded corners") - self.bbrounded_cb.setToolTip( - "If the bounding box is \n" - "to have rounded corners\n" - "their radius is equal to\n" - "the margin." - ) - grid4.addWidget(self.bbrounded_cb, 1, 0, 1, 2) - - -class ExcellonOptionsGroupUI(OptionsGroupUI): - def __init__(self, parent=None): - OptionsGroupUI.__init__(self, "Excellon Options", parent=parent) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.layout.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.layout.addLayout(grid0) - self.plot_cb = FCCheckBox(label='Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - grid0.addWidget(self.plot_cb, 0, 0) - self.solid_cb = FCCheckBox(label='Solid') - self.solid_cb.setToolTip( - "Solid circles." - ) - grid0.addWidget(self.solid_cb, 0, 1) - - ## Create CNC Job - self.cncjob_label = QtGui.QLabel('Create CNC Job') - self.cncjob_label.setToolTip( - "Create a CNC Job object\n" - "for this drill object." - ) - self.layout.addWidget(self.cncjob_label) - - grid1 = QtGui.QGridLayout() - self.layout.addLayout(grid1) - - cutzlabel = QtGui.QLabel('Cut Z:') - cutzlabel.setToolTip( - "Drill depth (negative)\n" - "below the copper surface." - ) - grid1.addWidget(cutzlabel, 0, 0) - self.cutz_entry = LengthEntry() - grid1.addWidget(self.cutz_entry, 0, 1) - - travelzlabel = QtGui.QLabel('Travel Z:') - travelzlabel.setToolTip( - "Tool height when travelling\n" - "across the XY plane." - ) - grid1.addWidget(travelzlabel, 1, 0) - self.travelz_entry = LengthEntry() - grid1.addWidget(self.travelz_entry, 1, 1) - - frlabel = QtGui.QLabel('Feed rate:') - frlabel.setToolTip( - "Tool speed while drilling\n" - "(in units per minute)." - ) - grid1.addWidget(frlabel, 2, 0) - 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, 4, 0) - self.spindlespeed_entry = IntEntry(allow_empty=True) - 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): - def __init__(self, parent=None): - OptionsGroupUI.__init__(self, "Geometry Options", parent=parent) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.layout.addWidget(self.plot_options_label) - - # Plot CB - self.plot_cb = FCCheckBox(label='Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - self.layout.addWidget(self.plot_cb) - - # ------------------------------ - ## Create CNC Job - # ------------------------------ - self.cncjob_label = QtGui.QLabel('Create CNC Job:') - self.cncjob_label.setToolTip( - "Create a CNC Job object\n" - "tracing the contours of this\n" - "Geometry object." - ) - self.layout.addWidget(self.cncjob_label) - - grid1 = QtGui.QGridLayout() - self.layout.addLayout(grid1) - - cutzlabel = QtGui.QLabel('Cut Z:') - cutzlabel.setToolTip( - "Cutting depth (negative)\n" - "below the copper surface." - ) - grid1.addWidget(cutzlabel, 0, 0) - self.cutz_entry = LengthEntry() - grid1.addWidget(self.cutz_entry, 0, 1) - - # Travel Z - travelzlabel = QtGui.QLabel('Travel Z:') - travelzlabel.setToolTip( - "Height of the tool when\n" - "moving without cutting." - ) - grid1.addWidget(travelzlabel, 1, 0) - self.travelz_entry = LengthEntry() - grid1.addWidget(self.travelz_entry, 1, 1) - - # Feedrate - frlabel = QtGui.QLabel('Feed Rate:') - frlabel.setToolTip( - "Cutting speed in the XY\n" - "plane in units per minute" - ) - grid1.addWidget(frlabel, 2, 0) - self.cncfeedrate_entry = LengthEntry() - grid1.addWidget(self.cncfeedrate_entry, 2, 1) - - # Tooldia - tdlabel = QtGui.QLabel('Tool dia:') - tdlabel.setToolTip( - "The diameter of the cutting\n" - "tool (just for display)." - ) - grid1.addWidget(tdlabel, 3, 0) - self.cnctooldia_entry = LengthEntry() - grid1.addWidget(self.cnctooldia_entry, 3, 1) - - spdlabel = QtGui.QLabel('Spindle speed:') - spdlabel.setToolTip( - "Speed of the spindle\n" - "in RPM (optional)" - ) - grid1.addWidget(spdlabel, 4, 0) - self.cncspindlespeed_entry = IntEntry(allow_empty=True) - grid1.addWidget(self.cncspindlespeed_entry, 4, 1) - - # ------------------------------ - ## Paint area - # ------------------------------ - self.paint_label = QtGui.QLabel('Paint Area:') - self.paint_label.setToolTip( - "Creates tool paths to cover the\n" - "whole area of a polygon (remove\n" - "all copper). You will be asked\n" - "to click on the desired polygon." - ) - self.layout.addWidget(self.paint_label) - - grid2 = QtGui.QGridLayout() - self.layout.addLayout(grid2) - - # Tool dia - ptdlabel = QtGui.QLabel('Tool dia:') - ptdlabel.setToolTip( - "Diameter of the tool to\n" - "be used in the operation." - ) - grid2.addWidget(ptdlabel, 0, 0) - - self.painttooldia_entry = LengthEntry() - grid2.addWidget(self.painttooldia_entry, 0, 1) - - # Overlap - ovlabel = QtGui.QLabel('Overlap:') - ovlabel.setToolTip( - "How much (fraction) of the tool\n" - "width to overlap each tool pass." - ) - grid2.addWidget(ovlabel, 1, 0) - self.paintoverlap_entry = LengthEntry() - grid2.addWidget(self.paintoverlap_entry, 1, 1) - - # Margin - marginlabel = QtGui.QLabel('Margin:') - marginlabel.setToolTip( - "Distance by which to avoid\n" - "the edges of the polygon to\n" - "be painted." - ) - grid2.addWidget(marginlabel, 2, 0) - self.paintmargin_entry = LengthEntry() - grid2.addWidget(self.paintmargin_entry, 2, 1) - - # Method - methodlabel = QtGui.QLabel('Method:') - methodlabel.setToolTip( - "Algorithm to paint the polygon:
" - "Standard: Fixed step inwards.
" - "Seed-based: Outwards from seed." - ) - grid2.addWidget(methodlabel, 3, 0) - self.paintmethod_combo = RadioSet([ - {"label": "Standard", "value": "standard"}, - {"label": "Seed-based", "value": "seed"}, - {"label": "Straight lines", "value": "lines"} - ], orientation='vertical') - grid2.addWidget(self.paintmethod_combo, 3, 1) - - # Connect lines - pathconnectlabel = QtGui.QLabel("Connect:") - pathconnectlabel.setToolTip( - "Draw lines between resulting\n" - "segments to minimize tool lifts." - ) - grid2.addWidget(pathconnectlabel, 4, 0) - self.pathconnect_cb = FCCheckBox() - grid2.addWidget(self.pathconnect_cb, 4, 1) - - # Paint contour - contourlabel = QtGui.QLabel("Contour:") - contourlabel.setToolTip( - "Cut around the perimeter of the polygon\n" - "to trim rough edges." - ) - grid2.addWidget(contourlabel, 5, 0) - self.contour_cb = FCCheckBox() - grid2.addWidget(self.contour_cb, 5, 1) - - # Polygon selection - selectlabel = QtGui.QLabel('Selection:') - selectlabel.setToolTip( - "How to select the polygons to paint." - ) - grid2.addWidget(selectlabel, 6, 0) - # grid3 = QtGui.QGridLayout() - self.selectmethod_combo = RadioSet([ - {"label": "Single", "value": "single"}, - {"label": "All", "value": "all"}, - # {"label": "Rectangle", "value": "rectangle"} - ]) - grid2.addWidget(self.selectmethod_combo, 6, 1) - - -class CNCJobOptionsGroupUI(OptionsGroupUI): - def __init__(self, parent=None): - OptionsGroupUI.__init__(self, "CNC Job Options", parent=None) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.layout.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.layout.addLayout(grid0) - - # Plot CB - # self.plot_cb = QtGui.QCheckBox('Plot') - self.plot_cb = FCCheckBox('Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - grid0.addWidget(self.plot_cb, 0, 0) - - # Tool dia for plot - tdlabel = QtGui.QLabel('Tool dia:') - tdlabel.setToolTip( - "Diameter of the tool to be\n" - "rendered in the plot." - ) - grid0.addWidget(tdlabel, 1, 0) - self.tooldia_entry = LengthEntry() - grid0.addWidget(self.tooldia_entry, 1, 1) - - ## Export G-Code - self.export_gcode_label = QtGui.QLabel("Export G-Code:") - self.export_gcode_label.setToolTip( - "Export and save G-Code to\n" - "make this object to a file." - ) - self.layout.addWidget(self.export_gcode_label) - - # Prepend to G-Code - prependlabel = QtGui.QLabel('Prepend to G-Code:') - prependlabel.setToolTip( - "Type here any G-Code commands you would\n" - "like to add at the beginning of the G-Code file." - ) - self.layout.addWidget(prependlabel) - - self.prepend_text = FCTextArea() - self.layout.addWidget(self.prepend_text) - - # Append text to G-Code - appendlabel = QtGui.QLabel('Append to G-Code:') - appendlabel.setToolTip( - "Type here any G-Code commands you would\n" - "like to append to the generated file.\n" - "I.e.: M2 (End of program)" - ) - self.layout.addWidget(appendlabel) - - 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): - """ - This is the app and project options editor. - """ - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent=parent) - - layout = QtGui.QVBoxLayout() - self.setLayout(layout) - - hlay1 = QtGui.QHBoxLayout() - layout.addLayout(hlay1) - unitslabel = QtGui.QLabel('Units:') - hlay1.addWidget(unitslabel) - self.units_radio = RadioSet([{'label': 'inch', 'value': 'IN'}, - {'label': 'mm', 'value': 'MM'}]) - hlay1.addWidget(self.units_radio) - - ####### Gerber ####### - # gerberlabel = QtGui.QLabel('Gerber Options') - # layout.addWidget(gerberlabel) - self.gerber_group = GerberOptionsGroupUI() - # self.gerber_group.setFrameStyle(QtGui.QFrame.StyledPanel) - layout.addWidget(self.gerber_group) - - ####### Excellon ####### - # excellonlabel = QtGui.QLabel('Excellon Options') - # layout.addWidget(excellonlabel) - self.excellon_group = ExcellonOptionsGroupUI() - # self.excellon_group.setFrameStyle(QtGui.QFrame.StyledPanel) - layout.addWidget(self.excellon_group) - - ####### Geometry ####### - # geometrylabel = QtGui.QLabel('Geometry Options') - # layout.addWidget(geometrylabel) - self.geometry_group = GeometryOptionsGroupUI() - # self.geometry_group.setStyle(QtGui.QFrame.StyledPanel) - layout.addWidget(self.geometry_group) - - ####### CNC ####### - # cnclabel = QtGui.QLabel('CNC Job Options') - # layout.addWidget(cnclabel) - self.cncjob_group = CNCJobOptionsGroupUI() - # self.cncjob_group.setStyle(QtGui.QFrame.StyledPanel) - layout.addWidget(self.cncjob_group) - -# def main(): -# -# app = QtGui.QApplication(sys.argv) -# fc = FlatCAMGUI() -# sys.exit(app.exec_()) -# -# -# if __name__ == '__main__': -# main() diff --git a/FlatCAMObj.py b/FlatCAMObj.py deleted file mode 100644 index 8529b600..00000000 --- a/FlatCAMObj.py +++ /dev/null @@ -1,1737 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from io import StringIO -from PyQt4 import QtCore -from copy import copy -from ObjectUI import * -import FlatCAMApp -import inspect # TODO: For debugging only. -from camlib import * -from FlatCAMCommon import LoudDict -from FlatCAMDraw import FlatCAMDraw - - -######################################## -## FlatCAMObj ## -######################################## -class FlatCAMObj(QtCore.QObject): - """ - Base type of objects handled in FlatCAM. These become interactive - in the GUI, can be plotted, and their options can be modified - by the user in their respective forms. - """ - - # Instance of the application to which these are related. - # The app should set this value. - app = None - - option_changed = QtCore.pyqtSignal(QtCore.QObject, str) - - def __init__(self, name): - """ - Constructor. - - :param name: Name of the object given by the user. - :return: FlatCAMObj - """ - QtCore.QObject.__init__(self) - - # View - self.ui = None - - self.options = LoudDict(name=name) - self.options.set_change_callback(self.on_options_change) - - self.form_fields = {} - - self.axes = None # Matplotlib axes - self.kind = None # Override with proper name - - self.muted_ui = False - - # assert isinstance(self.ui, ObjectUI) - # 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 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. - - :param d: Dictionary with attributes to set. - :return: None - """ - - 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) - self.option_changed.emit(self, key) - - def set_ui(self, ui): - self.ui = ui - - self.form_fields = {"name": self.ui.name_entry} - - assert isinstance(self.ui, ObjectUI) - self.ui.name_entry.returnPressed.connect(self.on_name_activate) - self.ui.offset_button.clicked.connect(self.on_offset_button_click) - self.ui.auto_offset_button.clicked.connect(self.on_auto_offset_button_click) - self.ui.scale_button.clicked.connect(self.on_scale_button_click) - self.ui.mirror_button.clicked.connect(self.on_mirror_button_click) - - def __str__(self): - return "".format(self.kind, self.options["name"]) - - def on_name_activate(self): - old_name = copy(self.options["name"]) - new_name = self.ui.name_entry.get_value() - self.options["name"] = self.ui.name_entry.get_value() - self.app.inform.emit("Name changed from %s to %s" % (old_name, new_name)) - - def on_offset_button_click(self): - self.app.report_usage("obj_on_offset_button") - - self.read_form() - vect = self.ui.offsetvector_entry.get_value() - self.offset(vect) - self.plot() - - def on_auto_offset_button_click(self): - self.app.report_usage("obj_on_auto_offset_button") - self.read_form() - minx, miny, maxx, maxy = self.bounds() - vect = (-minx, -miny) - self.ui.offsetvector_entry.set_value(vect) - self.offset(vect) - self.plot() - - def on_scale_button_click(self): - self.app.report_usage("obj_on_scale_button") - self.read_form() - factor = self.ui.scale_entry.get_value() - self.scale(factor) - self.plot() - - def on_mirror_button_click(self): - self.app.report_usage("obj_on_mirror_button") - self.read_form() - minx, miny, maxx, maxy = self.bounds() - - axis = self.ui.mirror_axis_radio.get_value() - - if not self.ui.mirror_auto_center_cb.get_value(): - vect = (0, 0) - elif axis == 'X': - vect = (0, (maxy + miny)/2) - else: - vect = ((maxx + minx)/2, 0) - - self.mirror(axis, vect) - 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: - try: - self.set_form_item(option) - except: - self.app.log.warning("Unexpected error:", sys.exc_info()) - - def read_form(self): - """ - Reads form into ``self.options``. - - :return: None - :rtype: None - """ - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()") - for option in self.options: - try: - self.read_form_item(option) - except: - self.app.log.warning("Unexpected error:", sys.exc_info()) - - def build_ui(self): - """ - Sets up the UI/form for this object. Show the UI - in the App. - - :return: None - :rtype: None - """ - - self.muted_ui = True - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()") - - # Remove anything else in the box - # box_children = self.app.ui.notebook.selected_contents.get_children() - # for child in box_children: - # self.app.ui.notebook.selected_contents.remove(child) - # while self.app.ui.selected_layout.count(): - # self.app.ui.selected_layout.takeAt(0) - - # Put in the UI - # box_selected.pack_start(sw, True, True, 0) - # self.app.ui.notebook.selected_contents.add(self.ui) - # self.app.ui.selected_layout.addWidget(self.ui) - try: - 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 - - def set_form_item(self, option): - """ - Copies the specified option to the UI form. - - :param option: Name of the option (Key in ``self.options``). - :type option: str - :return: None - """ - - try: - self.form_fields[option].set_value(self.options[option]) - except KeyError: - self.app.log.warning("Tried to set an option or field that does not exist: %s" % option) - - def read_form_item(self, option): - """ - Reads the specified option from the UI form into ``self.options``. - - :param option: Name of the option. - :type option: str - :return: None - """ - - try: - self.options[option] = self.form_fields[option].get_value() - 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). - Axes get created, appended to canvas and cleared before plotting. - Call this in descendants before doing the plotting. - - :return: Whether to continue plotting or not depending on the "plot" option. - :rtype: bool - """ - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()") - - # Axes must exist and be attached to canvas. - if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes: - self.axes = self.app.plotcanvas.new_axes(self.options['name']) - - if not self.options["plot"]: - self.axes.cla() - self.app.plotcanvas.auto_adjust_axes() - return False - - # Clear axes or we will plot on top of them. - self.axes.cla() # TODO: Thread safe? - return True - - def serialize(self): - """ - Returns a representation of the object as a dictionary so - it can be later exported as JSON. Override this method. - - :return: Dictionary representing the object - :rtype: dict - """ - return - - def deserialize(self, obj_dict): - """ - Re-builds an object from its serialized version. - - :param obj_dict: Dictionary representing a FlatCAMObj - :type obj_dict: dict - :return: None - """ - return - - -class FlatCAMGerber(FlatCAMObj, Gerber): - """ - Represents Gerber code. - """ - - ui_type = GerberObjectUI - - def __init__(self, name): - Gerber.__init__(self) - FlatCAMObj.__init__(self, name) - - self.kind = "gerber" - - # The 'name' is already in self.options from FlatCAMObj - # Automatically updates the UI - self.options.update({ - "plot": True, - "multicolored": False, - "solid": False, - "isotooldia": 0.016, - "isopasses": 1, - "isooverlap": 0.15, - "combine_passes": True, - "cutouttooldia": 0.07, - "cutoutmargin": 0.2, - "cutoutgapsize": 0.15, - "gaps": "tb", - "noncoppermargin": 0.0, - "noncopperrounded": False, - "bboxmargin": 0.0, - "bboxrounded": False - }) - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - - # 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_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) - - def set_ui(self, ui): - """ - Maps options with GUI inputs. - Connects GUI events to methods. - - :param ui: GUI object. - :type ui: GerberObjectUI - :return: None - """ - FlatCAMObj.set_ui(self, ui) - - FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()") - - self.form_fields.update({ - "plot": self.ui.plot_cb, - "multicolored": self.ui.multicolored_cb, - "solid": self.ui.solid_cb, - "isotooldia": self.ui.iso_tool_dia_entry, - "isopasses": self.ui.iso_width_entry, - "isooverlap": self.ui.iso_overlap_entry, - "combine_passes": self.ui.combine_passes_cb, - "cutouttooldia": self.ui.cutout_tooldia_entry, - "cutoutmargin": self.ui.cutout_margin_entry, - "cutoutgapsize": self.ui.cutout_gap_entry, - "gaps": self.ui.gaps_radio, - "noncoppermargin": self.ui.noncopper_margin_entry, - "noncopperrounded": self.ui.noncopper_rounded_cb, - "bboxmargin": self.ui.bbmargin_entry, - "bboxrounded": self.ui.bbrounded_cb - }) - - 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_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) - - def on_generatenoncopper_button_click(self, *args): - self.app.report_usage("gerber_on_generatenoncopper_button") - - self.read_form() - name = self.options["name"] + "_noncopper" - - def geo_init(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry) - bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"]) - if not self.options["noncopperrounded"]: - bounding_box = bounding_box.envelope - non_copper = bounding_box.difference(self.solid_geometry) - geo_obj.solid_geometry = non_copper - - # TODO: Check for None - self.app.new_object("geometry", name, geo_init) - - def on_generatebb_button_click(self, *args): - self.app.report_usage("gerber_on_generatebb_button") - self.read_form() - name = self.options["name"] + "_bbox" - - def geo_init(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry) - # Bounding box with rounded corners - bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"]) - if not self.options["bboxrounded"]: # Remove rounded corners - bounding_box = bounding_box.envelope - geo_obj.solid_geometry = bounding_box - - self.app.new_object("geometry", name, geo_init) - - def on_generatecutout_button_click(self, *args): - self.app.report_usage("gerber_on_generatecutout_button") - self.read_form() - name = self.options["name"] + "_cutout" - - def geo_init(geo_obj, app_obj): - margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2 - gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"] - minx, miny, maxx, maxy = self.bounds() - minx -= margin - maxx += margin - miny -= margin - maxy += margin - midx = 0.5 * (minx + maxx) - midy = 0.5 * (miny + maxy) - hgap = 0.5 * gap_size - pts = [[midx - hgap, maxy], - [minx, maxy], - [minx, midy + hgap], - [minx, midy - hgap], - [minx, miny], - [midx - hgap, miny], - [midx + hgap, miny], - [maxx, miny], - [maxx, midy - hgap], - [maxx, midy + hgap], - [maxx, maxy], - [midx + hgap, maxy]] - cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]], - [pts[6], pts[7], pts[10], pts[11]]], - "lr": [[pts[9], pts[10], pts[1], pts[2]], - [pts[3], pts[4], pts[7], pts[8]]], - "4": [[pts[0], pts[1], pts[2]], - [pts[3], pts[4], pts[5]], - [pts[6], pts[7], pts[8]], - [pts[9], pts[10], pts[11]]]} - cuts = cases[self.options['gaps']] - geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) - - # TODO: Check for None - self.app.new_object("geometry", name, geo_init) - - def on_iso_button_click(self, *args): - self.app.report_usage("gerber_on_iso_button") - self.read_form() - self.isolate() - - def follow(self, outname=None): - """ - Creates a geometry object "following" the gerber paths. - - :return: None - """ - - default_name = self.options["name"] + "_follow" - follow_name = outname or default_name - - def follow_init(follow_obj, app_obj): - # Propagate options - follow_obj.options["cnctooldia"] = self.options["isotooldia"] - follow_obj.solid_geometry = self.solid_geometry - app_obj.inform.emit("Follow geometry created: %s" % follow_obj.options["name"]) - - # TODO: Do something if this is None. Offer changing name? - self.app.new_object("geometry", follow_name, follow_init) - - def isolate(self, dia=None, passes=None, overlap=None, outname=None, combine=None): - """ - Creates an isolation routing geometry object in the project. - - :param dia: Tool diameter - :param passes: Number of tool widths to cut - :param overlap: Overlap between passes in fraction of tool diameter - :param outname: Base name of the output object - :return: None - """ - if dia is None: - dia = self.options["isotooldia"] - if passes is None: - passes = int(self.options["isopasses"]) - if overlap is None: - overlap = self.options["isooverlap"] - if combine is None: - combine = self.options["combine_passes"] - else: - combine = bool(combine) - - base_name = self.options["name"] + "_iso" - base_name = outname or base_name - - def generate_envelope(offset, invert): - # isolation_geometry produces an envelope that is going on the left of the geometry - # (the copper features). To leave the least amount of burrs on the features - # the tool needs to travel on the right side of the features (this is called conventional milling) - # the first pass is the one cutting all of the features, so it needs to be reversed - # the other passes overlap preceding ones and cut the left over copper. It is better for them - # to cut on the right side of the left over copper i.e on the left side of the features. - geom = self.isolation_geometry(offset) - if invert: - if type(geom) is MultiPolygon: - pl = [] - for p in geom: - pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) - geom = MultiPolygon(pl) - elif type(geom) is Polygon: - geom = Polygon(geom.exterior.coords[::-1], geom.interiors) - else: - raise str("Unexpected Geometry") - return geom - - if combine: - iso_name = base_name - - # TODO: This is ugly. Create way to pass data into init function. - def iso_init(geo_obj, app_obj): - # Propagate options - geo_obj.options["cnctooldia"] = self.options["isotooldia"] - 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) - geo_obj.solid_geometry.append(geom) - app_obj.inform.emit("Isolation geometry created: %s" % geo_obj.options["name"]) - - # TODO: Do something if this is None. Offer changing name? - self.app.new_object("geometry", iso_name, iso_init) - - else: - for i in range(passes): - - offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia - if passes > 1: - iso_name = base_name + str(i + 1) - else: - iso_name = base_name - - # TODO: This is ugly. Create way to pass data into init function. - 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) - app_obj.inform.emit("Isolation geometry created: %s" % geo_obj.options["name"]) - - # TODO: Do something if this is None. Offer changing name? - self.app.new_object("geometry", iso_name, iso_init) - - def on_plot_cb_click(self, *args): - if self.muted_ui: - return - self.read_form_item('plot') - self.plot() - - def on_solid_cb_click(self, *args): - if self.muted_ui: - return - self.read_form_item('solid') - self.plot() - - def on_multicolored_cb_click(self, *args): - if self.muted_ui: - return - self.read_form_item('multicolored') - self.plot() - - def convert_units(self, units): - """ - Converts the units of the object by scaling dimensions in all geometry - and options. - - :param units: Units to which to convert the object: "IN" or "MM". - :type units: str - :return: None - :rtype: None - """ - - factor = Gerber.convert_units(self, units) - - self.options['isotooldia'] *= factor - self.options['cutoutmargin'] *= factor - self.options['cutoutgapsize'] *= factor - self.options['noncoppermargin'] *= factor - self.options['bboxmargin'] *= factor - - def plot(self): - - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()") - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - geometry = self.solid_geometry - - # Make sure geometry is iterable. - try: - _ = iter(geometry) - except TypeError: - geometry = [geometry] - - if self.options["multicolored"]: - linespec = '-' - else: - linespec = 'k-' - - if self.options["solid"]: - for poly in geometry: - # TODO: Too many things hardcoded. - try: - patch = PolygonPatch(poly, - facecolor="#BBF268", - edgecolor="#006E20", - alpha=0.75, - zorder=2) - self.axes.add_patch(patch) - except AssertionError: - FlatCAMApp.App.log.warning("A geometry component was not a polygon:") - FlatCAMApp.App.log.warning(str(poly)) - else: - for poly in geometry: - x, y = poly.exterior.xy - self.axes.plot(x, y, linespec) - for ints in poly.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, linespec) - - self.app.plotcanvas.auto_adjust_axes() - - def serialize(self): - return { - "options": self.options, - "kind": self.kind - } - - -class FlatCAMExcellon(FlatCAMObj, Excellon): - """ - Represents Excellon/Drill code. - """ - - ui_type = ExcellonObjectUI - - def __init__(self, name): - Excellon.__init__(self) - FlatCAMObj.__init__(self, name) - - self.kind = "excellon" - - self.options.update({ - "plot": True, - "solid": False, - "drillz": -0.1, - "travelz": 0.1, - "feedrate": 5.0, - # "toolselection": "" - "tooldia": 0.1, - "toolchange": False, - "toolchangez": 1.0, - "spindlespeed": None - }) - - # TODO: Document this. - self.tool_cbs = {} - - # Attributes to be included in serialization - # Always append to it because it carries contents - # 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 list(exc.tools.copy().keys()): - 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 list(exc_final.tools.copy().keys()): - 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 list(toolsrework.copy().keys()): - 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) - - # Populate tool list - n = len(self.tools) - self.ui.tools_table.setColumnCount(3) - self.ui.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Count']) - self.ui.tools_table.setRowCount(n) - self.ui.tools_table.setSortingEnabled(False) - - i = 0 - for tool in self.tools: - - drill_cnt = 0 # variable to store the nr of drills per tool - # Find no of drills for the current tool - for drill in self.drills: - if drill.get('tool') == tool: - drill_cnt += 1 - - id = QtGui.QTableWidgetItem(tool) - id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - self.ui.tools_table.setItem(i, 0, id) # Tool name/id - dia = QtGui.QTableWidgetItem(str(self.tools[tool]['C'])) - dia.setFlags(QtCore.Qt.ItemIsEnabled) - drill_count = QtGui.QTableWidgetItem('%d' % drill_cnt) - drill_count.setFlags(QtCore.Qt.ItemIsEnabled) - self.ui.tools_table.setItem(i, 1, dia) # Diameter - self.ui.tools_table.setItem(i, 2, drill_count) # Number of drills per tool - 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() - horizontal_header = self.ui.tools_table.horizontalHeader() - horizontal_header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents) - horizontal_header.setResizeMode(1, QtGui.QHeaderView.Stretch) - horizontal_header.setResizeMode(2, QtGui.QHeaderView.ResizeToContents) - # horizontal_header.setStretchLastSection(True) - self.ui.tools_table.verticalHeader().hide() - self.ui.tools_table.setSortingEnabled(True) - - def set_ui(self, ui): - """ - Configures the user interface for this object. - Connects options to form fields. - - :param ui: User interface object. - :type ui: ExcellonObjectUI - :return: None - """ - FlatCAMObj.set_ui(self, ui) - - FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()") - - self.form_fields.update({ - "plot": self.ui.plot_cb, - "solid": self.ui.solid_cb, - "drillz": self.ui.cutz_entry, - "travelz": self.ui.travelz_entry, - "feedrate": self.ui.feedrate_entry, - "tooldia": self.ui.tooldia_entry, - "toolchange": self.ui.toolchange_cb, - "toolchangez": self.ui.toolchangez_entry, - "spindlespeed": self.ui.spindlespeed_entry - }) - - assert isinstance(self.ui, ExcellonObjectUI), \ - "Expected a ExcellonObjectUI, got %s" % type(self.ui) - self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) - self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) - self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click) - self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click) - - def get_selected_tools_list(self): - """ - Returns the keys to the self.tools dictionary corresponding - to the selections on the tool list in the GUI. - - :return: List of tools. - :rtype: list - """ - return [str(x.text()) for x in self.ui.tools_table.selectedItems()] - - def generate_milling(self, tools=None, outname=None, tooldia=None): - """ - Note: This method is a good template for generic operations as - it takes it's options from parameters or otherwise from the - object's options and returns a (success, msg) tuple as feedback - for shell operations. - - :return: Success/failure condition tuple (bool, str). - :rtype: tuple - """ - - # Get the tools from the list. These are keys - # to self.tools - if tools is None: - tools = self.get_selected_tools_list() - - if outname is None: - outname = self.options["name"] + "_mill" - - if tooldia is None: - tooldia = self.options["tooldia"] - - # Sort tools by diameter. items() -> [('name', diameter), ...] - # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) - - # Python3 no longer allows direct comparison between dicts so we need to sort tools differently - sort = [] - for k, v in self.tools.items(): - sort.append((k, v.get('C'))) - sorted_tools = sorted(sort, key=lambda t1: t1[1]) - log.debug("Tools are sorted: %s" % str(sorted_tools)) - - if tools == "all": - tools = [i[0] for i in sorted_tools] # List if ordered tool names. - log.debug("Tools 'all' and sorted are: %s" % str(tools)) - - if len(tools) == 0: - self.app.inform.emit("Please select one or more tools from the list and try again.") - return False, "Error: No tools." - - for tool in tools: - if self.tools[tool]["C"] < tooldia: - self.app.inform.emit("[warning] Milling tool is larger than hole size. Cancelled.") - return False, "Error: Milling tool is larger than hole." - - def geo_init(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) - app_obj.progress.emit(20) - - geo_obj.solid_geometry = [] - - for hole in self.drills: - if hole['tool'] in tools: - geo_obj.solid_geometry.append( - Point(hole['point']).buffer(self.tools[hole['tool']]["C"] / 2 - - tooldia / 2).exterior - ) - - def geo_thread(app_obj): - app_obj.new_object("geometry", outname, geo_init) - app_obj.progress.emit(100) - - # Create a promise with the new name - self.app.collection.promise(outname) - - # Send to worker - self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) - - return True, "" - - def on_generate_milling_button_click(self, *args): - self.app.report_usage("excellon_on_create_milling_button") - self.read_form() - - self.generate_milling() - - def on_create_cncjob_button_click(self, *args): - self.app.report_usage("excellon_on_create_cncjob_button") - self.read_form() - - # Get the tools from the list - tools = self.get_selected_tools_list() - - if len(tools) == 0: - self.app.inform.emit("Please select one or more tools from the list and try again.") - return - - job_name = self.options["name"] + "_cnc" - - # Object initialization function for app.new_object() - def job_init(job_obj, app_obj): - assert isinstance(job_obj, FlatCAMCNCjob), \ - "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) - - app_obj.progress.emit(20) - job_obj.z_cut = self.options["drillz"] - job_obj.z_move = self.options["travelz"] - job_obj.feedrate = self.options["feedrate"] - job_obj.spindlespeed = self.options["spindlespeed"] - # There could be more than one drill size... - # job_obj.tooldia = # TODO: duplicate variable! - # job_obj.options["tooldia"] = - - tools_csv = ','.join(tools) - job_obj.generate_from_excellon_by_tool(self, tools_csv, - toolchange=self.options["toolchange"], - toolchangez=self.options["toolchangez"]) - - app_obj.progress.emit(50) - job_obj.gcode_parse() - - app_obj.progress.emit(60) - job_obj.create_geometry() - - app_obj.progress.emit(80) - - # To be run in separate thread - def job_thread(app_obj): - app_obj.new_object("cncjob", job_name, job_init) - app_obj.progress.emit(100) - - # Create promise for the new name. - self.app.collection.promise(job_name) - - # Send to worker - # self.app.worker.add_task(job_thread, [self.app]) - self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) - - def on_plot_cb_click(self, *args): - if self.muted_ui: - return - self.read_form_item('plot') - self.plot() - - def on_solid_cb_click(self, *args): - if self.muted_ui: - return - self.read_form_item('solid') - self.plot() - - def convert_units(self, units): - factor = Excellon.convert_units(self, units) - - self.options['drillz'] *= factor - self.options['travelz'] *= factor - self.options['feedrate'] *= factor - - def plot(self): - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - try: - _ = iter(self.solid_geometry) - except TypeError: - self.solid_geometry = [self.solid_geometry] - - # Plot excellon (All polygons?) - if self.options["solid"]: - for geo in self.solid_geometry: - patch = PolygonPatch(geo, - facecolor="#C40000", - edgecolor="#750000", - alpha=0.75, - zorder=3) - self.axes.add_patch(patch) - else: - for geo in self.solid_geometry: - x, y = geo.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in geo.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'g-') - - self.app.plotcanvas.auto_adjust_axes() - - -class FlatCAMCNCjob(FlatCAMObj, CNCjob): - """ - Represents G-Code. - """ - - ui_type = CNCObjectUI - - def __init__(self, name, units="in", kind="generic", z_move=0.1, - feedrate=3.0, z_cut=-0.002, tooldia=0.0, - spindlespeed=None): - - FlatCAMApp.App.log.debug("Creating CNCJob object...") - - CNCjob.__init__(self, units=units, kind=kind, z_move=z_move, - feedrate=feedrate, z_cut=z_cut, tooldia=tooldia, - spindlespeed=spindlespeed) - - FlatCAMObj.__init__(self, name) - - self.kind = "cncjob" - - self.options.update({ - "plot": True, - "tooldia": 0.4 / 25.4, # 0.4mm in inches - "append": "", - "prepend": "", - "dwell": False, - "dwelltime": 1 - }) - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - - def set_ui(self, ui): - FlatCAMObj.set_ui(self, ui) - - FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()") - - assert isinstance(self.ui, CNCObjectUI), \ - "Expected a CNCObjectUI, got %s" % type(self.ui) - - self.form_fields.update({ - "plot": self.ui.plot_cb, - "tooldia": self.ui.tooldia_entry, - "append": self.ui.append_text, - "prepend": self.ui.prepend_text, - "postprocess": self.ui.process_script, - "dwell": self.ui.dwell_cb, - "dwelltime": self.ui.dwelltime_entry - }) - - 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) - - def on_updateplot_button_click(self, *args): - """ - Callback for the "Updata Plot" button. Reads the form for updates - and plots the object. - """ - self.read_form() - self.plot() - - def on_exportgcode_button_click(self, *args): - self.app.report_usage("cncjob_on_exportgcode_button") - - self.read_form() - - try: - filename = str(QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...", - directory=self.app.defaults["last_folder"])) - except TypeError: - filename = str(QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...")) - - preamble = str(self.ui.prepend_text.get_value()) - postamble = str(self.ui.append_text.get_value()) - processor = str(self.ui.process_script.get_value()) - - self.export_gcode(filename, preamble=preamble, postamble=postamble, processor=processor) - - 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='', processor=''): - - 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 - self.read_form_item('plot') - self.plot() - - def plot(self): - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - self.plot2(self.axes, tooldia=self.options["tooldia"]) - - self.app.plotcanvas.auto_adjust_axes() - - def convert_units(self, units): - factor = CNCjob.convert_units(self, units) - FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()") - self.options["tooldia"] *= factor - - -class FlatCAMGeometry(FlatCAMObj, Geometry): - """ - Geometric object not associated with a specific - format. - """ - - ui_type = GeometryObjectUI - - @staticmethod - def merge(geo_list, geo_final): - """ - Merges the geometry of objects in geo_list into - the geometry of geo_final. - - :param geo_list: List of FlatCAMGeometry Objects to join. - :param geo_final: Destination FlatCAMGeometry object. - :return: None - """ - - if geo_final.solid_geometry is None: - geo_final.solid_geometry = [] - if type(geo_final.solid_geometry) is not list: - geo_final.solid_geometry = [geo_final.solid_geometry] - - for geo in geo_list: - - # Expand lists - if type(geo) is list: - FlatCAMGeometry.merge(geo, geo_final) - - # If not list, just append - 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) - - def __init__(self, name): - FlatCAMObj.__init__(self, name) - Geometry.__init__(self) - - self.kind = "geometry" - - self.options.update({ - "plot": True, - "cutz": -0.002, - "travelz": 0.1, - "feedrate": 5.0, - "spindlespeed": None, - "cnctooldia": 0.4 / 25.4, - "painttooldia": 0.0625, - "paintoverlap": 0.15, - "paintmargin": 0.01, - "paintmethod": "standard", - "pathconnect": True, - "paintcontour": True, - "multidepth": False, - "depthperpass": 0.002, - "selectmethod": "single" - }) - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - - def build_ui(self): - FlatCAMObj.build_ui(self) - - def set_ui(self, ui): - FlatCAMObj.set_ui(self, ui) - - FlatCAMApp.App.log.debug("FlatCAMGeometry.set_ui()") - - assert isinstance(self.ui, GeometryObjectUI), \ - "Expected a GeometryObjectUI, got %s" % type(self.ui) - - self.form_fields.update({ - "plot": self.ui.plot_cb, - "cutz": self.ui.cutz_entry, - "travelz": self.ui.travelz_entry, - "feedrate": self.ui.cncfeedrate_entry, - "spindlespeed": self.ui.cncspindlespeed_entry, - "cnctooldia": self.ui.cnctooldia_entry, - "painttooldia": self.ui.painttooldia_entry, - "paintoverlap": self.ui.paintoverlap_entry, - "paintmargin": self.ui.paintmargin_entry, - "paintmethod": self.ui.paintmethod_combo, - "pathconnect": self.ui.pathconnect_cb, - "paintcontour": self.ui.paintcontour_cb, - "multidepth": self.ui.mpass_cb, - "depthperpass": self.ui.maxdepth_entry, - "selectmethod": self.ui.selectmethod_combo - }) - - 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.read_form() - tooldia = self.options["painttooldia"] - overlap = self.options["paintoverlap"] - - if self.options["selectmethod"] == "all": - self.paint_poly_all(tooldia, overlap, - connect=self.options["pathconnect"], - contour=self.options["paintcontour"]) - return - - if self.options["selectmethod"] == "single": - self.app.inform.emit("Click inside the desired polygon.") - - # To be called after clicking on the plot. - def doit(event): - self.app.inform.emit("Painting polygon...") - self.app.plotcanvas.mpl_disconnect(subscription) - point = [event.xdata, event.ydata] - self.paint_poly_single_click(point, tooldia, overlap, - connect=self.options["pathconnect"], - contour=self.options["paintcontour"]) - - subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit) - - def paint_poly_single_click(self, inside_pt, tooldia, overlap, - outname=None, connect=True, contour=True): - """ - Paints a polygon selected by clicking on its interior. - - Note: - * The margin is taken directly from the form. - - :param inside_pt: [x, y] - :param tooldia: Diameter of the painting tool - :param overlap: Overlap of the tool between passes. - :param outname: Name of the resulting Geometry Object. - :param connect: Connect lines to avoid tool lifts. - :param contour: Paint around the edges. - :return: None - """ - - # Which polygon. - poly = self.find_polygon(inside_pt) - - # No polygon? - if poly is None: - self.app.log.warning('No polygon found.') - self.app.inform.emit('[warning] No polygon found.') - return - - proc = self.app.proc_container.new("Painting polygon.") - - name = outname or self.options["name"] + "_paint" - - # Initializes the new geometry object - 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) - - if self.options["paintmethod"] == "seed": - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, connect=connect, - contour=contour) - - elif self.options["paintmethod"] == "lines": - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon3(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, connect=connect, - contour=contour) - else: - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, connect=connect, - contour=contour) - - if cp is not None: - geo_obj.solid_geometry = list(cp.get_objects()) - - geo_obj.options["cnctooldia"] = tooldia - - # Experimental... - print("Indexing...") - geo_obj.make_index() - print("Done") - - self.app.inform.emit("Done.") - - def job_thread(app_obj): - try: - app_obj.new_object("geometry", name, gen_paintarea) - except Exception as e: - proc.done() - raise e - proc.done() - - self.app.inform.emit("Polygon Paint started ...") - - # Promise object with the new name - self.app.collection.promise(name) - - # Background - self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) - - def paint_poly_all(self, tooldia, overlap, outname=None, - connect=True, contour=True): - """ - Paints all polygons in this object. - - :param tooldia: - :param overlap: - :param outname: - :param connect: Connect lines to avoid tool lifts. - :param contour: Paint around the edges. - :return: - """ - - proc = self.app.proc_container.new("Painting polygon.") - - name = outname or self.options["name"] + "_paint" - - # This is a recursive generator of individual Polygons. - # Note: Double check correct implementation. Might exit - # early if it finds something that is not a Polygon? - def recurse(geo): - try: - for subg in geo: - for subsubg in recurse(subg): - yield subsubg - except TypeError: - if isinstance(geo, Polygon): - yield geo - - raise StopIteration - - # Initializes the new geometry object - def gen_paintarea(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) - - geo_obj.solid_geometry = [] - - for poly in recurse(self.solid_geometry): - - if self.options["paintmethod"] == "seed": - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, contour=contour, - connect=connect) - - elif self.options["paintmethod"] == "lines": - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon3(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, contour=contour, - connect=connect) - - else: - # Type(cp) == FlatCAMRTreeStorage | None - cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]), - tooldia, overlap=overlap, contour=contour, - connect=connect) - - if cp is not None: - geo_obj.solid_geometry += list(cp.get_objects()) - - geo_obj.options["cnctooldia"] = tooldia - - # Experimental... - print("Indexing...") - geo_obj.make_index() - print("Done") - - self.app.inform.emit("Done.") - - def job_thread(app_obj): - try: - app_obj.new_object("geometry", name, gen_paintarea) - except Exception as e: - proc.done() - traceback.print_stack() - raise e - proc.done() - - self.app.inform.emit("Polygon Paint started ...") - - # Promise object with the new name - self.app.collection.promise(name) - - # Background - self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) - - def on_generatecnc_button_click(self, *args): - self.app.report_usage("geometry_on_generatecnc_button") - self.read_form() - self.generatecncjob() - - def generatecncjob(self, - z_cut=None, - z_move=None, - feedrate=None, - tooldia=None, - outname=None, - spindlespeed=None, - multidepth=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 - `generate_from_geometry_2()` method. - - :param z_cut: Cut depth (negative) - :param z_move: Hight of the tool when travelling (not cutting) - :param feedrate: Feed rate while cutting - :param tooldia: Tool diameter - :param outname: Name of the new object - :param spindlespeed: Spindle speed (RPM) - :return: None - """ - - outname = outname if outname is not None else self.options["name"] + "_cnc" - z_cut = z_cut if z_cut is not None else self.options["cutz"] - z_move = z_move if z_move is not None else self.options["travelz"] - feedrate = feedrate if feedrate is not None else self.options["feedrate"] - tooldia = tooldia if tooldia is not None else self.options["cnctooldia"] - multidepth = multidepth if multidepth is not None else self.options["multidepth"] - depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"] - - # To allow default value to be "" (optional in gui) and translate to None - # if not isinstance(spindlespeed, int): - # if isinstance(self.options["spindlespeed"], int) or \ - # isinstance(self.options["spindlespeed"], float): - # spindlespeed = int(self.options["spindlespeed"]) - # else: - # spindlespeed = None - - if spindlespeed is None: - # int or None. - spindlespeed = self.options['spindlespeed'] - - # Object initialization function for app.new_object() - # RUNNING ON SEPARATE THREAD! - def job_init(job_obj, app_obj): - assert isinstance(job_obj, FlatCAMCNCjob), \ - "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) - - # Propagate options - job_obj.options["tooldia"] = tooldia - - app_obj.progress.emit(20) - job_obj.z_cut = z_cut - job_obj.z_move = z_move - job_obj.feedrate = feedrate - job_obj.spindlespeed = spindlespeed - app_obj.progress.emit(40) - # TODO: The tolerance should not be hard coded. Just for testing. - job_obj.generate_from_geometry_2(self, - multidepth=multidepth, - depthpercut=depthperpass, - tolerance=0.0005) - - app_obj.progress.emit(50) - job_obj.gcode_parse() - - app_obj.progress.emit(80) - - 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) - - # 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: - return - self.read_form_item('plot') - self.plot() - - def scale(self, factor): - """ - Scales all geometry by a given factor. - - :param factor: Factor by which to scale the object's geometry/ - :type factor: float - :return: None - :rtype: None - """ - - if type(self.solid_geometry) == list: - self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0)) - for g in self.solid_geometry] - else: - self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor, - origin=(0, 0)) - - def offset(self, vect): - """ - Offsets all geometry by a given vector/ - - :param vect: (x, y) vector by which to offset the object's geometry. - :type vect: tuple - :return: None - :rtype: None - """ - - dx, dy = vect - - 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) - - self.options['cutz'] *= factor - self.options['travelz'] *= factor - self.options['feedrate'] *= factor - self.options['cnctooldia'] *= factor - self.options['painttooldia'] *= factor - self.options['paintmargin'] *= factor - - return factor - - def plot_element(self, element): - try: - for sub_el in element: - self.plot_element(sub_el) - - except TypeError: # Element is not iterable... - - if type(element) == Polygon: - x, y = element.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in element.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'r-') - return - - if type(element) == LineString or type(element) == LinearRing: - x, y = element.coords.xy - self.axes.plot(x, y, 'r-') - return - - FlatCAMApp.App.log.warning("Did not plot:" + str(type(element))) - - def plot(self): - """ - Plots the object into its axes. If None, of if the axes - are not part of the app's figure, it fetches new ones. - - :return: None - """ - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - # Make sure solid_geometry is iterable. - # TODO: This method should not modify the object !!! - # try: - # _ = iter(self.solid_geometry) - # except TypeError: - # if self.solid_geometry is None: - # self.solid_geometry = [] - # else: - # self.solid_geometry = [self.solid_geometry] - # - # for geo in self.solid_geometry: - # - # if type(geo) == Polygon: - # x, y = geo.exterior.coords.xy - # self.axes.plot(x, y, 'r-') - # for ints in geo.interiors: - # x, y = ints.coords.xy - # self.axes.plot(x, y, 'r-') - # continue - # - # if type(geo) == LineString or type(geo) == LinearRing: - # x, y = geo.coords.xy - # self.axes.plot(x, y, 'r-') - # continue - # - # if type(geo) == MultiPolygon: - # for poly in geo: - # x, y = poly.exterior.coords.xy - # self.axes.plot(x, y, 'r-') - # for ints in poly.interiors: - # x, y = ints.coords.xy - # self.axes.plot(x, y, 'r-') - # continue - # - # FlatCAMApp.App.log.warning("Did not plot:", str(type(geo))) - - self.plot_element(self.solid_geometry) - - self.app.plotcanvas.auto_adjust_axes() diff --git a/FlatCAMProcess.py b/FlatCAMProcess.py deleted file mode 100644 index 05f3a766..00000000 --- a/FlatCAMProcess.py +++ /dev/null @@ -1,156 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from FlatCAMGUI import FlatCAMActivityView -from PyQt4 import QtCore -import weakref - - -# import logging - -# log = logging.getLogger('base2') -# #log.setLevel(logging.DEBUG) -# log.setLevel(logging.WARNING) -# #log.setLevel(logging.INFO) -# formatter = logging.Formatter('[%(levelname)s] %(message)s') -# handler = logging.StreamHandler() -# handler.setFormatter(formatter) -# log.addHandler(handler) - - -class FCProcess(object): - - app = None - - def __init__(self, descr): - self.callbacks = { - "done": [] - } - self.descr = descr - self.status = "Active" - - def __del__(self): - self.done() - - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type is not None: - self.app.log.error("Abnormal termination of process!") - self.app.log.error(exc_type) - self.app.log.error(exc_val) - self.app.log.error(exc_tb) - - self.done() - - def done(self): - for fcn in self.callbacks["done"]: - fcn(self) - - def connect(self, callback, event="done"): - if callback not in self.callbacks[event]: - self.callbacks[event].append(callback) - - def disconnect(self, callback, event="done"): - try: - self.callbacks[event].remove(callback) - except ValueError: - pass - - def set_status(self, status_string): - self.status = status_string - - def status_msg(self): - return self.descr - - -class FCProcessContainer(object): - """ - This is the process container, or controller (as in MVC) - of the Process/Activity tracking. - - FCProcessContainer keeps weak references to the FCProcess'es - such that their __del__ method is called when the user - looses track of their reference. - """ - - app = None - - def __init__(self): - - self.procs = [] - - def add(self, proc): - - self.procs.append(weakref.ref(proc)) - - def new(self, descr): - proc = FCProcess(descr) - - proc.connect(self.on_done, event="done") - - self.add(proc) - - self.on_change(proc) - - return proc - - def on_change(self, proc): - pass - - def on_done(self, proc): - self.remove(proc) - - def remove(self, proc): - - to_be_removed = [] - - for pref in self.procs: - if pref() == proc or pref() is None: - to_be_removed.append(pref) - - for pref in to_be_removed: - self.procs.remove(pref) - - -class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer): - something_changed = QtCore.pyqtSignal() - - def __init__(self, view): - assert isinstance(view, FlatCAMActivityView), \ - "Expected a FlatCAMActivityView, got %s" % type(view) - - FCProcessContainer.__init__(self) - QtCore.QObject.__init__(self) - - self.view = view - - self.something_changed.connect(self.update_view) - - def on_done(self, proc): - self.app.log.debug("FCVisibleProcessContainer.on_done()") - super(FCVisibleProcessContainer, self).on_done(proc) - - self.something_changed.emit() - - def on_change(self, proc): - self.app.log.debug("FCVisibleProcessContainer.on_change()") - super(FCVisibleProcessContainer, self).on_change(proc) - - self.something_changed.emit() - - def update_view(self): - if len(self.procs) == 0: - self.view.set_idle() - - elif len(self.procs) == 1: - self.view.set_busy(self.procs[0]().status_msg()) - - else: - self.view.set_busy("%d processes running." % len(self.procs)) \ No newline at end of file diff --git a/FlatCAMShell.py b/FlatCAMShell.py deleted file mode 100644 index d07495d8..00000000 --- a/FlatCAMShell.py +++ /dev/null @@ -1,33 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -import termwidget - - -class FCShell(termwidget.TermWidget): - def __init__(self, sysShell, *args): - termwidget.TermWidget.__init__(self, *args) - self._sysShell = sysShell - - def is_command_complete(self, text): - def skipQuotes(text): - quote = text[0] - text = text[1:] - endIndex = str(text).index(quote) - return text[endIndex:] - while text: - if text[0] in ('"', "'"): - try: - text = skipQuotes(text) - except ValueError: - return False - text = text[1:] - return True - - def child_exec_command(self, text): - self._sysShell.exec_command(text) diff --git a/FlatCAMTool.py b/FlatCAMTool.py deleted file mode 100644 index 98b3a13b..00000000 --- a/FlatCAMTool.py +++ /dev/null @@ -1,80 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from PyQt4 import QtGui - - -class FlatCAMTool(QtGui.QWidget): - - toolName = "FlatCAM Generic Tool" - - def __init__(self, app, parent=None): - """ - - :param app: The application this tool will run in. - :type app: App - :param parent: Qt Parent - :return: FlatCAMTool - """ - QtGui.QWidget.__init__(self, parent) - - # self.setSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Maximum) - - self.layout = QtGui.QVBoxLayout() - self.setLayout(self.layout) - - self.app = app - - self.menuAction = None - - def install(self, icon=None, separator=None, **kwargs): - before = None - - # 'pos' is the menu where the Action has to be installed - # if no 'pos' kwarg is provided then by default our Action will be installed in the menutool - # as it previously was - if 'pos' in kwargs: - pos = kwargs['pos'] - else: - pos = self.app.ui.menutool - - # 'before' is the Action in the menu stated by 'pos' kwarg, before which we want our Action to be installed - # if 'before' kwarg is not provided, by default our Action will be added in the last place. - if 'before' in kwargs: - before = (kwargs['before']) - - # create the new Action - self.menuAction = QtGui.QAction(self) - # if provided, add an icon to this Action - if icon is not None: - self.menuAction.setIcon(icon) - # set the text name of the Action, which will be displayed in the menu - self.menuAction.setText(self.toolName) - # add a ToolTip to the new Action - # self.menuAction.setToolTip(self.toolTip) # currently not available - - # insert the action in the position specified by 'before' and 'pos' kwargs - pos.insertAction(before, self.menuAction) - - # if separator parameter is True add a Separator after the newly created Action - if separator is True: - pos.addSeparator() - - self.menuAction.triggered.connect(self.run) - - def run(self): - # Remove anything else in the GUI - self.app.ui.tool_scroll_area.takeWidget() - - # Put ourself in the GUI - self.app.ui.tool_scroll_area.setWidget(self) - - # Switch notebook to tool page - self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab) - - self.show() diff --git a/FlatCAMVersion.py b/FlatCAMVersion.py deleted file mode 100644 index ba9e04a5..00000000 --- a/FlatCAMVersion.py +++ /dev/null @@ -1,34 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -################################################# -# FlatCAM - Version settings # -################################################# - -import logging - -version = { - "number": 8.5, - "date": (2016, 7, 1), # Year, Month, Day - "name": None, - "release": False, -} - - -def setup(app): - app.version = version["number"] - app.version_date = version["date"] - if version["release"]: - app.log.setLevel(logging.WARNING) - else: - app.log.setLevel(logging.DEBUG) - - if version["name"] is None and version["release"] == False: - app.version_name = "Development Version" - else: - app.version_name = version["name"] diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py deleted file mode 100644 index 29192fc3..00000000 --- a/FlatCAMWorker.py +++ /dev/null @@ -1,66 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from PyQt4 import QtCore - - -class Worker(QtCore.QObject): - """ - Implements a queue of tasks to be carried out in order - 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): - - 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): - - self.app.log.debug("Running task: %s" % str(task)) - - 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 - - self.app.log.debug("Task ignored.") diff --git a/GUIElements.py b/GUIElements.py deleted file mode 100644 index 42fd3270..00000000 --- a/GUIElements.py +++ /dev/null @@ -1,407 +0,0 @@ -from PyQt4 import QtGui, QtCore -from copy import copy -#import FlatCAMApp -import re -import logging - -log = logging.getLogger('base') - - -class RadioSet(QtGui.QWidget): - def __init__(self, choices, orientation='horizontal', parent=None): - """ - The choices are specified as a list of dictionaries containing: - - * 'label': Shown in the UI - * 'value': The value returned is selected - - :param choices: List of choices. See description. - :param orientation: 'horizontal' (default) of 'vertical'. - :param parent: Qt parent widget. - :type choices: list - """ - super(RadioSet, self).__init__(parent) - self.choices = copy(choices) - - if orientation == 'horizontal': - layout = QtGui.QHBoxLayout() - else: - layout = QtGui.QVBoxLayout() - - group = QtGui.QButtonGroup(self) - - for choice in self.choices: - choice['radio'] = QtGui.QRadioButton(choice['label']) - group.addButton(choice['radio']) - layout.addWidget(choice['radio'], stretch=0) - choice['radio'].toggled.connect(self.on_toggle) - - layout.addStretch() - self.setLayout(layout) - - self.group_toggle_fn = lambda: None - - def on_toggle(self): - log.debug("Radio toggled") - radio = self.sender() - if radio.isChecked(): - self.group_toggle_fn() - return - - def get_value(self): - for choice in self.choices: - if choice['radio'].isChecked(): - return choice['value'] - log.error("No button was toggled in RadioSet.") - return None - - def set_value(self, val): - for choice in self.choices: - if choice['value'] == val: - choice['radio'].setChecked(True) - return - log.error("Value given is not part of this RadioSet: %s" % str(val)) - - -class LengthEntry(QtGui.QLineEdit): - def __init__(self, output_units='IN', parent=None): - super(LengthEntry, self).__init__(parent) - - self.output_units = output_units - self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$") - - # Unit conversion table OUTPUT-INPUT - self.scales = { - 'IN': {'IN': 1.0, - 'MM': 1/25.4}, - 'MM': {'IN': 25.4, - 'MM': 1.0} - } - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(LengthEntry, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(LengthEntry, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def returnPressed(self, *args, **kwargs): - val = self.get_value() - if val is not None: - self.set_text(str(val)) - else: - log.warning("Could not interpret entry: %s" % self.get_text()) - - def get_value(self): - raw = str(self.text()).strip(' ') - # match = self.format_re.search(raw) - - try: - units = raw[-2:] - units = self.scales[self.output_units][units.upper()] - value = raw[:-2] - return float(eval(value))*units - except IndexError: - value = raw - return float(eval(value)) - except KeyError: - value = raw - return float(eval(value)) - except: - log.warning("Could not parse value in entry: %s" % str(raw)) - return None - - def set_value(self, val): - self.setText(str(val)) - - -class FloatEntry(QtGui.QLineEdit): - def __init__(self, parent=None): - super(FloatEntry, self).__init__(parent) - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(FloatEntry, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(FloatEntry, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def returnPressed(self, *args, **kwargs): - val = self.get_value() - if val is not None: - self.set_text(str(val)) - else: - log.warning("Could not interpret entry: %s" % self.text()) - - def get_value(self): - raw = str(self.text()).strip(' ') - try: - evaled = eval(raw) - except: - log.error("Could not evaluate: %s" % str(raw)) - return None - - return float(evaled) - - def set_value(self, val): - self.setText("%.6f" % val) - - -class IntEntry(QtGui.QLineEdit): - - def __init__(self, parent=None, allow_empty=False, empty_val=None): - super(IntEntry, self).__init__(parent) - self.allow_empty = allow_empty - self.empty_val = empty_val - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(IntEntry, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(IntEntry, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def get_value(self): - - if self.allow_empty: - if str(self.text()) == "": - return self.empty_val - - return int(self.text()) - - def set_value(self, val): - - if val == self.empty_val and self.allow_empty: - self.setText("") - return - - self.setText(str(val)) - - -class FCEntry(QtGui.QLineEdit): - def __init__(self, parent=None): - super(FCEntry, self).__init__(parent) - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(FCEntry, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(FCEntry, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def get_value(self): - return str(self.text()) - - def set_value(self, val): - self.setText(str(val)) - - -class EvalEntry(QtGui.QLineEdit): - def __init__(self, parent=None): - super(EvalEntry, self).__init__(parent) - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(EvalEntry, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(EvalEntry, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def returnPressed(self, *args, **kwargs): - val = self.get_value() - if val is not None: - self.setText(str(val)) - else: - log.warning("Could not interpret entry: %s" % self.get_text()) - - def get_value(self): - raw = str(self.text()).strip(' ') - try: - return eval(raw) - except: - log.error("Could not evaluate: %s" % str(raw)) - return None - - def set_value(self, val): - self.setText(str(val)) - - -class FCCheckBox(QtGui.QCheckBox): - def __init__(self, label='', parent=None): - super(FCCheckBox, self).__init__(str(label), parent) - - def get_value(self): - return self.isChecked() - - 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): - super(FCTextArea, self).__init__(parent) - - def set_value(self, val): - self.setPlainText(val) - - def get_value(self): - return str(self.toPlainText()) - -class FCInputDialog(QtGui.QInputDialog): - def __init__(self, parent=None, ok=False, val=None): - super(FCInputDialog, self).__init__(parent) - self.allow_empty = ok - self.empty_val = val - self.readyToEdit = True - - def mousePressEvent(self, e, Parent=None): - # required to deselect on 2nd click - super(FCInputDialog, self).mousePressEvent(e) - if self.readyToEdit: - self.selectAll() - self.readyToEdit = False - - def focusOutEvent(self, e): - # required to remove cursor on focusOut - super(FCInputDialog, self).focusOutEvent(e) - self.deselect() - self.readyToEdit = True - - def get_value(self, title=None, message=None, min=None, max=None, decimals=None): - if title is None: - title = "FlatCAM action" - if message is None: - message = "Please enter the value: " - if min is None: - min = 0.0 - if max is None: - max = 100.0 - if decimals is None: - decimals = 1 - self.val,self.ok = self.getDouble(self, title, message, min=min, - max=max, decimals=decimals) - return [self.val,self.ok] - - def set_value(self, val): - pass - - -class FCButton(QtGui.QPushButton): - def __init__(self, parent=None): - super(FCButton, self).__init__(parent) - - def get_value(self): - return self.isChecked() - - def set_value(self, val): - self.setText(str(val)) - - -class VerticalScrollArea(QtGui.QScrollArea): - """ - This widget extends QtGui.QScrollArea to make a vertical-only - scroll area that also expands horizontally to accomodate - its contents. - """ - def __init__(self, parent=None): - QtGui.QScrollArea.__init__(self, parent=parent) - self.setWidgetResizable(True) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - - def eventFilter(self, source, event): - """ - The event filter gets automatically installed when setWidget() - is called. - - :param source: - :param event: - :return: - """ - if event.type() == QtCore.QEvent.Resize and source == self.widget(): - # log.debug("VerticalScrollArea: Widget resized:") - # log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width()) - # log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width()) - - self.setMinimumWidth(self.widget().sizeHint().width() + - self.verticalScrollBar().sizeHint().width()) - - # if self.verticalScrollBar().isVisible(): - # log.debug(" Scroll bar visible") - # self.setMinimumWidth(self.widget().minimumSizeHint().width() + - # self.verticalScrollBar().width()) - # else: - # log.debug(" Scroll bar hidden") - # self.setMinimumWidth(self.widget().minimumSizeHint().width()) - return QtGui.QWidget.eventFilter(self, source, event) - - -class OptionalInputSection: - - def __init__(self, cb, optinputs): - """ - Associates the a checkbox with a set of inputs. - - :param cb: Checkbox that enables the optional inputs. - :param optinputs: List of widgets that are optional. - :return: - """ - assert isinstance(cb, FCCheckBox), \ - "Expected an FCCheckBox, got %s" % type(cb) - - self.cb = cb - self.optinputs = optinputs - - self.on_cb_change() - self.cb.stateChanged.connect(self.on_cb_change) - - def on_cb_change(self): - - if self.cb.checkState(): - - for widget in self.optinputs: - widget.setEnabled(True) - - else: - - for widget in self.optinputs: - widget.setEnabled(False) - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index ba87cb83..00000000 --- a/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014-2016 Juan Pablo Caram - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 44bcd867..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -recursive-include share *.png -recursive-include share *.svg - -include flatcam.desktop -include LICENSE -include README.md -include *.py -include MANIFEST.in - diff --git a/ObjectCollection.py b/ObjectCollection.py deleted file mode 100644 index 6485ca11..00000000 --- a/ObjectCollection.py +++ /dev/null @@ -1,362 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from FlatCAMObj import * -import inspect # TODO: Remove -import FlatCAMApp -from PyQt4 import Qt, QtGui, QtCore - - -class KeySensitiveListView(QtGui.QListView): - """ - QtGui.QListView extended to emit a signal on key press. - """ - - keyPressed = QtCore.pyqtSignal(int) - - def keyPressEvent(self, event): - super(KeySensitiveListView, self).keyPressEvent(event) - self.keyPressed.emit(event.key()) - - -#class ObjectCollection(QtCore.QAbstractListModel): -class ObjectCollection(): - """ - Object storage and management. - """ - - classdict = { - "gerber": FlatCAMGerber, - "excellon": FlatCAMExcellon, - "cncjob": FlatCAMCNCjob, - "geometry": FlatCAMGeometry - } - - icon_files = { - "gerber": "share:flatcam_icon16.png", - "excellon": "share:drill16.png", - "cncjob": "share:cnc16.png", - "geometry": "share:geometry16.png" - } - - def __init__(self, parent=None): - #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]) - - ### Data ### - self.object_list = [] - self.checked_indexes = [] - - # Names of objects that are expected to become available. - # For example, when the creation of a new object will run - # in the background and will complete some time in the - # future. This is a way to reserve the name and to let other - # tasks know that they have to wait until available. - self.promises = set() - - ### View - #self.view = QtGui.QListView() - self.view = KeySensitiveListView() - self.view.setSelectionMode(Qt.QAbstractItemView.ExtendedSelection) - self.model = QtGui.QStandardItemModel(self.view) - self.view.setModel(self.model) - self.model.itemChanged.connect(self.on_item_changed) - - self.click_modifier = None - - ## GUI Events - self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change) - self.view.activated.connect(self.on_item_activated) - self.view.keyPressed.connect(self.on_key) - self.view.clicked.connect(self.on_mouse_down) - - def promise(self, obj_name): - FlatCAMApp.App.log.debug("Object %s has been promised." % obj_name) - self.promises.add(obj_name) - - def has_promises(self): - return len(self.promises) > 0 - - def on_key(self, key): - - # Delete - if key == QtCore.Qt.Key_Delete: - # Delete via the application to - # ensure cleanup of the GUI - self.get_active().app.on_delete() - return - - if key == QtCore.Qt.Key_Space: - self.get_active().ui.plot_cb.toggle() - return - - def print_list(self): - for obj in self.object_list: - print(obj) - - def on_mouse_down(self, event): - FlatCAMApp.App.log.debug("Mouse button pressed on list") - #self.print_list() - - def rowCount(self, parent=QtCore.QModelIndex(), *args, **kwargs): - return len(self.object_list) - - def columnCount(self, *args, **kwargs): - return 1 - - 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 - - def append(self, obj, active=False): - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()") - - name = obj.options["name"] - - # Check promises and clear if exists - if name in self.promises: - self.promises.remove(name) - FlatCAMApp.App.log.debug("Promised object %s became available." % name) - FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises)) - - # Prevent same name - while name in self.get_names(): - ## Create a new name - # Ends with number? - FlatCAMApp.App.log.debug("new_object(): Object name (%s) exists, changing." % name) - match = re.search(r'(.*[^\d])?(\d+)$', name) - if match: # Yes: Increment the number! - base = match.group(1) or '' - num = int(match.group(2)) - name = base + str(num + 1) - else: # No: add a number! - name += "_1" - obj.options["name"] = name - - obj.set_ui(obj.ui_type()) - - # Required before appending (Qt MVC) - #self.beginInsertRows(QtCore.QModelIndex(), len(self.object_list), len(self.object_list)) - - # Simply append to the python list - self.object_list.append(obj) - - # Create the model item to insert into the QListView - icon = QtGui.QIcon(self.icons[obj.kind])#self.icons["gerber"]) - item = QtGui.QStandardItem(icon, str(name)) - # Item is not editable, so that double click - # does not allow cell value modification. - item.setEditable(False) - # The item is checkable, to add the checkbox. - item.setCheckable(True) - if obj.options["plot"] is True: - item.setCheckState(2) #Qt.Checked) - else: - item.setCheckState(0) #Qt.Unchecked) - - self.model.appendRow(item) - - obj.option_changed.connect(self.on_object_option_changed) - - # Required after appending (Qt MVC) - #self.endInsertRows() - - def on_object_option_changed(self, obj, key): - if key == "plot": - self.model.blockSignals(True) - name = obj.options["name"] - state = 0 #Qt.Unchecked - for index in range(self.model.rowCount()): - item = self.model.item(index) - if self.object_list[item.row()].options["name"] == name: - if obj.options["plot"] == True: - state = 2 #Qt.Checked - - item.setCheckState(state) - obj.ui.plot_cb.set_value(state) - break - self.model.blockSignals(False) - - def get_names(self): - """ - Gets a list of the names of all objects in the collection. - - :return: List of names. - :rtype: list - """ - - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()") - return [x.options['name'] for x in self.object_list] - - def get_bounds(self): - """ - Finds coordinates bounding all objects in the collection. - - :return: [xmin, ymin, xmax, ymax] - :rtype: list - """ - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()") - - # TODO: Move the operation out of here. - - xmin = Inf - ymin = Inf - xmax = -Inf - ymax = -Inf - - for obj in self.object_list: - try: - gxmin, gymin, gxmax, gymax = obj.bounds() - xmin = min([xmin, gxmin]) - ymin = min([ymin, gymin]) - xmax = max([xmax, gxmax]) - ymax = max([ymax, gymax]) - except: - FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.") - - return [xmin, ymin, xmax, ymax] - - def get_by_name(self, name): - """ - Fetches the FlatCAMObj with the given `name`. - - :param name: The name of the object. - :type name: str - :return: The requested object or None if no such object. - :rtype: FlatCAMObj or None - """ - FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()") - - for obj in self.object_list: - if obj.options['name'] == name: - return obj - return None - - def delete_active(self): - selections = self.view.selectedIndexes() - if len(selections) == 0: - return - row = selections[0].row() - - #self.beginRemoveRows(QtCore.QModelIndex(), row, row) - - self.object_list.pop(row) - self.model.removeRow(row) - - #self.endRemoveRows() - - def get_active(self): - """ - Returns the active object or None - - :return: FlatCAMObj or None - """ - selections = self.view.selectedIndexes() - if len(selections) == 0: - return None - row = selections[0].row() - return self.object_list[row] - - def get_selected(self): - """ - Returns list of objects selected in the view. - - :return: List of objects - """ - return [self.object_list[sel.row()] for sel in self.view.selectedIndexes()] - - def set_active(self, name): - """ - Selects object by name from the project list. This triggers the - list_selection_changed event and call on_list_selection_changed. - - :param name: Name of the FlatCAM Object - :return: None - """ - iobj = self.model.createIndex(self.get_names().index(name), 0) # Column 0 - self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select) - - 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. - - :param name: Name of the FlatCAM Object - :return: None - """ - iobj = self.model.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: - selection_index = current.indexes()[0].row() - except IndexError: - FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)") - return - - self.object_list[selection_index].build_ui() - - def on_item_changed(self, item): - FlatCAMApp.App.log.debug("on_item_changed(): " + str(item.row()) + " " + self.object_list[item.row()].options["name"]) - if item.checkState() == QtCore.Qt.Checked: - self.object_list[item.row()].options["plot"] = True #(item.checkState() == QtCore.Qt.Checked) - else: - self.object_list[item.row()].options["plot"] = False #(item.checkState() == QtCore.Qt.Checked) - - self.object_list[item.row()].plot() - return - - def on_item_activated(self, index): - """ - Double-click or Enter on item. - - :param index: Index of the item in the list. - :return: None - """ - 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.model.removeRows(0, self.model.rowCount()) - self.object_list = [] - self.checked_indexes = [] - -# self.endResetModel() - - def get_list(self): - return self.object_list - diff --git a/ObjectUI.py b/ObjectUI.py deleted file mode 100644 index 9d7fafef..00000000 --- a/ObjectUI.py +++ /dev/null @@ -1,875 +0,0 @@ -import sys -from PyQt4 import QtGui, QtCore -#from GUIElements import * -from GUIElements import FCEntry, FloatEntry, EvalEntry, FCCheckBox, \ - LengthEntry, FCTextArea, IntEntry, RadioSet, OptionalInputSection - - -class ObjectUI(QtGui.QWidget): - """ - Base class for the UI of FlatCAM objects. Deriving classes should - put UI elements in ObjectUI.custom_box (QtGui.QLayout). - """ - - def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object', parent=None): - QtGui.QWidget.__init__(self, parent=parent) - - layout = QtGui.QVBoxLayout() - self.setLayout(layout) - - ## Page Title box (spacing between children) - self.title_box = QtGui.QHBoxLayout() - layout.addLayout(self.title_box) - - ## Page Title icon - pixmap = QtGui.QPixmap(icon_file) - self.icon = QtGui.QLabel() - self.icon.setPixmap(pixmap) - self.title_box.addWidget(self.icon, stretch=0) - - ## Title label - self.title_label = QtGui.QLabel("" + title + "") - self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) - self.title_box.addWidget(self.title_label, stretch=1) - - ## Object name - self.name_box = QtGui.QHBoxLayout() - layout.addLayout(self.name_box) - name_label = QtGui.QLabel("Name:") - self.name_box.addWidget(name_label) - self.name_entry = FCEntry() - self.name_box.addWidget(self.name_entry) - - ## Box box for custom widgets - # This gets populated in offspring implementations. - self.custom_box = QtGui.QVBoxLayout() - layout.addLayout(self.custom_box) - - ########################### - ## Common to all objects ## - ########################### - - #### Scale #### - self.scale_label = QtGui.QLabel('Scale:') - self.scale_label.setToolTip( - "Change the size of the object." - ) - layout.addWidget(self.scale_label) - - self.scale_grid = QtGui.QGridLayout() - layout.addLayout(self.scale_grid) - - # Factor - faclabel = QtGui.QLabel('Factor:') - faclabel.setToolTip( - "Factor by which to multiply\n" - "geometric features of this object." - ) - self.scale_grid.addWidget(faclabel, 0, 0) - self.scale_entry = FloatEntry() - self.scale_entry.set_value(1.0) - self.scale_grid.addWidget(self.scale_entry, 0, 1) - - # GO Button - self.scale_button = QtGui.QPushButton('Scale') - self.scale_button.setToolTip( - "Perform scaling operation." - ) - layout.addWidget(self.scale_button) - - #### Offset #### - self.offset_label = QtGui.QLabel('Offset:') - self.offset_label.setToolTip( - "Change the position of this object." - ) - layout.addWidget(self.offset_label) - - self.offset_grid = QtGui.QGridLayout() - layout.addLayout(self.offset_grid) - - self.offset_vectorlabel = QtGui.QLabel('Vector:') - self.offset_vectorlabel.setToolTip( - "Amount by which to move the object\n" - "in the x and y axes in (x, y) format." - ) - self.offset_grid.addWidget(self.offset_vectorlabel, 0, 0) - self.offsetvector_entry = EvalEntry() - self.offsetvector_entry.setText("(0.0, 0.0)") - self.offset_grid.addWidget(self.offsetvector_entry, 0, 1) - - self.offset_button = QtGui.QPushButton('Offset') - self.offset_button.setToolTip( - "Perform the offset operation." - ) - layout.addWidget(self.offset_button) - - self.auto_offset_button = QtGui.QPushButton('Offset auto') - self.auto_offset_button.setToolTip( - "Align the object with the x and y axes." - ) - layout.addWidget(self.auto_offset_button) - - #### Mirror #### - self.mirror_label = QtGui.QLabel('Mirror:') - self.mirror_label.setToolTip( - "Flip the object along an axis." - ) - layout.addWidget(self.mirror_label) - - self.mirror_axis_grid = QtGui.QGridLayout() - layout.addLayout(self.mirror_axis_grid) - - axislabel = QtGui.QLabel('Axis:') - axislabel.setToolTip( - "Mirror axis parallel to the x or y axis." - ) - self.mirror_axis_grid.addWidget(axislabel, 0, 0) - - self.mirror_axis_radio = RadioSet([{'label': 'X', 'value': 'X'}, - {'label': 'Y', 'value': 'Y'}]) - self.mirror_axis_radio.set_value('Y') - self.mirror_axis_grid.addWidget(self.mirror_axis_radio, 0, 1) - - self.mirror_auto_center_cb = FCCheckBox(label='Center axis automatically') - self.mirror_auto_center_cb.setToolTip( - "Place the mirror axis on the middle of the object." - ) - self.mirror_auto_center_cb.set_value(True) - layout.addWidget(self.mirror_auto_center_cb) - - self.mirror_button = QtGui.QPushButton('Mirror') - self.mirror_button.setToolTip( - "Perform the mirror operation." - ) - layout.addWidget(self.mirror_button) - - layout.addStretch() - - -class CNCObjectUI(ObjectUI): - """ - User interface for CNCJob objects. - """ - - def __init__(self, parent=None): - """ - Creates the user interface for CNCJob objects. GUI elements should - be placed in ``self.custom_box`` to preserve the layout. - """ - - ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png', parent=parent) - - # Scale and offset are not available for CNCJob objects. - # Hiding from the GUI. - for i in range(0, self.scale_grid.count()): - self.scale_grid.itemAt(i).widget().hide() - self.scale_label.hide() - self.scale_button.hide() - - for i in range(0, self.offset_grid.count()): - self.offset_grid.itemAt(i).widget().hide() - self.offset_label.hide() - self.offset_button.hide() - self.auto_offset_button.hide() - - self.mirror_label.hide() - for i in range(0, self.mirror_axis_grid.count()): - self.mirror_axis_grid.itemAt(i).widget().hide() - self.mirror_auto_center_cb.hide() - self.mirror_button.hide() - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.custom_box.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.custom_box.addLayout(grid0) - - # Plot CB - # self.plot_cb = QtGui.QCheckBox('Plot') - self.plot_cb = FCCheckBox('Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - grid0.addWidget(self.plot_cb, 0, 0) - - # Tool dia for plot - tdlabel = QtGui.QLabel('Tool dia:') - tdlabel.setToolTip( - "Diameter of the tool to be\n" - "rendered in the plot." - ) - grid0.addWidget(tdlabel, 1, 0) - self.tooldia_entry = LengthEntry() - grid0.addWidget(self.tooldia_entry, 1, 1) - - # Update plot button - self.updateplot_button = QtGui.QPushButton('Update Plot') - self.updateplot_button.setToolTip( - "Update the plot." - ) - 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" - "make this object to a file." - ) - self.custom_box.addWidget(self.export_gcode_label) - - # Prepend text to Gerber - prependlabel = QtGui.QLabel('Prepend to G-Code:') - prependlabel.setToolTip( - "Type here any G-Code commands you would\n" - "like to add to the beginning of the generated file." - ) - self.custom_box.addWidget(prependlabel) - - self.prepend_text = FCTextArea() - self.custom_box.addWidget(self.prepend_text) - - # Append text to Gerber - appendlabel = QtGui.QLabel('Append to G-Code:') - appendlabel.setToolTip( - "Type here any G-Code commands you would\n" - "like to append to the generated file.\n" - "I.e.: M2 (End of program)" - ) - self.custom_box.addWidget(appendlabel) - - self.append_text = FCTextArea() - self.custom_box.addWidget(self.append_text) - - processorlabel = QtGui.QLabel('Postprocessing-Script:') - processorlabel.setToolTip( - "Enter a Postprocessing Script here.\n" - "It gets applied to the G-Code after it\n" - "is generated." - ) - self.custom_box.addWidget(processorlabel) - self.process_script = FCTextArea() - self.custom_box.addWidget(self.process_script) - - - # 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( - "Opens dialog to save G-Code\n" - "file." - ) - self.custom_box.addWidget(self.export_gcode_button) - - -class GeometryObjectUI(ObjectUI): - """ - User interface for Geometry objects. - """ - - def __init__(self, parent=None): - super(GeometryObjectUI, self).__init__(title='Geometry Object', icon_file='share/geometry32.png', parent=parent) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.custom_box.addWidget(self.plot_options_label) - - # Plot CB - self.plot_cb = FCCheckBox(label='Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - self.custom_box.addWidget(self.plot_cb) - - #----------------------------------- - # Create CNC Job - #----------------------------------- - self.cncjob_label = QtGui.QLabel('Create CNC Job:') - self.cncjob_label.setToolTip( - "Create a CNC Job object\n" - "tracing the contours of this\n" - "Geometry object." - ) - self.custom_box.addWidget(self.cncjob_label) - - grid1 = QtGui.QGridLayout() - self.custom_box.addLayout(grid1) - - cutzlabel = QtGui.QLabel('Cut Z:') - cutzlabel.setToolTip( - "Cutting depth (negative)\n" - "below the copper surface." - ) - grid1.addWidget(cutzlabel, 0, 0) - self.cutz_entry = LengthEntry() - grid1.addWidget(self.cutz_entry, 0, 1) - - # Travel Z - travelzlabel = QtGui.QLabel('Travel Z:') - travelzlabel.setToolTip( - "Height of the tool when\n" - "moving without cutting." - ) - grid1.addWidget(travelzlabel, 1, 0) - self.travelz_entry = LengthEntry() - grid1.addWidget(self.travelz_entry, 1, 1) - - # Feedrate - frlabel = QtGui.QLabel('Feed Rate:') - frlabel.setToolTip( - "Cutting speed in the XY\n" - "plane in units per minute" - ) - grid1.addWidget(frlabel, 2, 0) - self.cncfeedrate_entry = LengthEntry() - grid1.addWidget(self.cncfeedrate_entry, 2, 1) - - # Tooldia - tdlabel = QtGui.QLabel('Tool dia:') - tdlabel.setToolTip( - "The diameter of the cutting\n" - "tool (just for display)." - ) - grid1.addWidget(tdlabel, 3, 0) - self.cnctooldia_entry = LengthEntry() - grid1.addWidget(self.cnctooldia_entry, 3, 1) - - # Spindlespeed - spdlabel = QtGui.QLabel('Spindle speed:') - spdlabel.setToolTip( - "Speed of the spindle\n" - "in RPM (optional)" - ) - grid1.addWidget(spdlabel, 4, 0) - self.cncspindlespeed_entry = IntEntry(allow_empty=True) - grid1.addWidget(self.cncspindlespeed_entry, 4, 1) - - # Multi-pass - mpasslabel = QtGui.QLabel('Multi-Depth:') - mpasslabel.setToolTip( - "Use multiple passes to limit\n" - "the cut depth in each pass. Will\n" - "cut multiple times until Cut Z is\n" - "reached." - ) - grid1.addWidget(mpasslabel, 5, 0) - self.mpass_cb = FCCheckBox() - grid1.addWidget(self.mpass_cb, 5, 1) - - maxdepthlabel = QtGui.QLabel('Depth/pass:') - maxdepthlabel.setToolTip( - "Depth of each pass (positive)." - ) - grid1.addWidget(maxdepthlabel, 6, 0) - self.maxdepth_entry = LengthEntry() - grid1.addWidget(self.maxdepth_entry, 6, 1) - - self.ois_mpass = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry]) - - # Button - self.generate_cnc_button = QtGui.QPushButton('Generate') - self.generate_cnc_button.setToolTip( - "Generate the CNC Job object." - ) - self.custom_box.addWidget(self.generate_cnc_button) - - #------------------------------ - # Paint area - #------------------------------ - self.paint_label = QtGui.QLabel('Paint Area:') - self.paint_label.setToolTip( - "Creates tool paths to cover the\n" - "whole area of a polygon (remove\n" - "all copper). You will be asked\n" - "to click on the desired polygon." - ) - self.custom_box.addWidget(self.paint_label) - - grid2 = QtGui.QGridLayout() - self.custom_box.addLayout(grid2) - - # Tool dia - ptdlabel = QtGui.QLabel('Tool dia:') - ptdlabel.setToolTip( - "Diameter of the tool to\n" - "be used in the operation." - ) - grid2.addWidget(ptdlabel, 0, 0) - - self.painttooldia_entry = LengthEntry() - grid2.addWidget(self.painttooldia_entry, 0, 1) - - # Overlap - ovlabel = QtGui.QLabel('Overlap:') - ovlabel.setToolTip( - "How much (fraction) of the tool\n" - "width to overlap each tool pass." - ) - grid2.addWidget(ovlabel, 1, 0) - self.paintoverlap_entry = LengthEntry() - grid2.addWidget(self.paintoverlap_entry, 1, 1) - - # Margin - marginlabel = QtGui.QLabel('Margin:') - marginlabel.setToolTip( - "Distance by which to avoid\n" - "the edges of the polygon to\n" - "be painted." - ) - grid2.addWidget(marginlabel, 2, 0) - self.paintmargin_entry = LengthEntry() - grid2.addWidget(self.paintmargin_entry, 2, 1) - - # Method - methodlabel = QtGui.QLabel('Method:') - methodlabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) - methodlabel.setToolTip( - "Algorithm to paint the polygon:
" - "Standard: Fixed step inwards.
" - "Seed-based: Outwards from seed." - ) - grid2.addWidget(methodlabel, 3, 0) - self.paintmethod_combo = RadioSet([ - {"label": "Standard", "value": "standard"}, - {"label": "Seed-based", "value": "seed"}, - {"label": "Straight lines", "value": "lines"} - ], orientation='vertical') - grid2.addWidget(self.paintmethod_combo, 3, 1) - - # Connect lines - pathconnectlabel = QtGui.QLabel("Connect:") - pathconnectlabel.setToolTip( - "Draw lines between resulting\n" - "segments to minimize tool lifts." - ) - grid2.addWidget(pathconnectlabel, 4, 0) - self.pathconnect_cb = FCCheckBox() - grid2.addWidget(self.pathconnect_cb, 4, 1) - - contourlabel = QtGui.QLabel("Contour:") - contourlabel.setToolTip( - "Cut around the perimeter of the polygon\n" - "to trim rough edges." - ) - grid2.addWidget(contourlabel, 5, 0) - self.paintcontour_cb = FCCheckBox() - grid2.addWidget(self.paintcontour_cb, 5, 1) - - # Polygon selection - selectlabel = QtGui.QLabel('Selection:') - selectlabel.setToolTip( - "How to select the polygons to paint." - ) - grid2.addWidget(selectlabel, 6, 0) - #grid3 = QtGui.QGridLayout() - self.selectmethod_combo = RadioSet([ - {"label": "Single", "value": "single"}, - {"label": "All", "value": "all"}, - #{"label": "Rectangle", "value": "rectangle"} - ]) - grid2.addWidget(self.selectmethod_combo, 6, 1) - - # GO Button - self.generate_paint_button = QtGui.QPushButton('Generate') - self.generate_paint_button.setToolTip( - "After clicking here, click inside\n" - "the polygon you wish to be painted.\n" - "A new Geometry object with the tool\n" - "paths will be created." - ) - self.custom_box.addWidget(self.generate_paint_button) - - -class ExcellonObjectUI(ObjectUI): - """ - User interface for Excellon objects. - """ - - def __init__(self, parent=None): - ObjectUI.__init__(self, title='Excellon Object', - icon_file='share/drill32.png', - parent=parent) - - #### Plot options #### - - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.custom_box.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.custom_box.addLayout(grid0) - self.plot_cb = FCCheckBox(label='Plot') - self.plot_cb.setToolTip( - "Plot (show) this object." - ) - grid0.addWidget(self.plot_cb, 0, 0) - self.solid_cb = FCCheckBox(label='Solid') - self.solid_cb.setToolTip( - "Solid circles." - ) - grid0.addWidget(self.solid_cb, 0, 1) - - #### Tools #### - - self.tools_table_label = QtGui.QLabel('Tools') - self.tools_table_label.setToolTip( - "Tools in this Excellon object." - ) - self.custom_box.addWidget(self.tools_table_label) - self.tools_table = QtGui.QTableWidget() - self.tools_table.setFixedHeight(100) - self.custom_box.addWidget(self.tools_table) - - #### Create CNC Job #### - - self.cncjob_label = QtGui.QLabel('Create CNC Job') - self.cncjob_label.setToolTip( - "Create a CNC Job object\n" - "for this drill object." - ) - self.custom_box.addWidget(self.cncjob_label) - - grid1 = QtGui.QGridLayout() - self.custom_box.addLayout(grid1) - - cutzlabel = QtGui.QLabel('Cut Z:') - cutzlabel.setToolTip( - "Drill depth (negative)\n" - "below the copper surface." - ) - grid1.addWidget(cutzlabel, 0, 0) - self.cutz_entry = LengthEntry() - grid1.addWidget(self.cutz_entry, 0, 1) - - travelzlabel = QtGui.QLabel('Travel Z:') - travelzlabel.setToolTip( - "Tool height when travelling\n" - "across the XY plane." - ) - grid1.addWidget(travelzlabel, 1, 0) - self.travelz_entry = LengthEntry() - grid1.addWidget(self.travelz_entry, 1, 1) - - frlabel = QtGui.QLabel('Feed rate:') - frlabel.setToolTip( - "Tool speed while drilling\n" - "(in units per minute)." - ) - grid1.addWidget(frlabel, 2, 0) - self.feedrate_entry = LengthEntry() - grid1.addWidget(self.feedrate_entry, 2, 1) - - # Tool change: - toolchlabel = QtGui.QLabel("Tool change:") - toolchlabel.setToolTip( - "Include tool-change sequence\n" - "in G-Code (Pause for tool change)." - ) - self.toolchange_cb = FCCheckBox() - grid1.addWidget(toolchlabel, 3, 0) - grid1.addWidget(self.toolchange_cb, 3, 1) - - # Tool change Z: - toolchzlabel = QtGui.QLabel("Tool change Z:") - toolchzlabel.setToolTip( - "Z-axis position (height) for\n" - "tool change." - ) - grid1.addWidget(toolchzlabel, 4, 0) - self.toolchangez_entry = LengthEntry() - grid1.addWidget(self.toolchangez_entry, 4, 1) - self.ois_tcz = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry]) - - # Spindlespeed - spdlabel = QtGui.QLabel('Spindle speed:') - spdlabel.setToolTip( - "Speed of the spindle\n" - "in RPM (optional)" - ) - grid1.addWidget(spdlabel, 5, 0) - self.spindlespeed_entry = IntEntry(allow_empty=True) - grid1.addWidget(self.spindlespeed_entry, 5, 1) - - choose_tools_label = QtGui.QLabel( - "Select from the tools section above\n" - "the tools you want to include." - ) - self.custom_box.addWidget(choose_tools_label) - - self.generate_cnc_button = QtGui.QPushButton('Generate') - self.generate_cnc_button.setToolTip( - "Generate the CNC Job." - ) - self.custom_box.addWidget(self.generate_cnc_button) - - #### Milling Holes #### - self.mill_hole_label = QtGui.QLabel('Mill Holes') - self.mill_hole_label.setToolTip( - "Create Geometry for milling holes." - ) - self.custom_box.addWidget(self.mill_hole_label) - - grid1 = QtGui.QGridLayout() - self.custom_box.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) - - choose_tools_label2 = QtGui.QLabel( - "Select from the tools section above\n" - "the tools you want to include." - ) - self.custom_box.addWidget(choose_tools_label2) - - self.generate_milling_button = QtGui.QPushButton('Generate Geometry') - self.generate_milling_button.setToolTip( - "Create the Geometry Object\n" - "for milling toolpaths." - ) - self.custom_box.addWidget(self.generate_milling_button) - - -class GerberObjectUI(ObjectUI): - """ - User interface for Gerber objects. - """ - - def __init__(self, parent=None): - ObjectUI.__init__(self, title='Gerber Object', parent=parent) - - ## Plot options - self.plot_options_label = QtGui.QLabel("Plot Options:") - self.custom_box.addWidget(self.plot_options_label) - - grid0 = QtGui.QGridLayout() - self.custom_box.addLayout(grid0) - # Plot CB - self.plot_cb = FCCheckBox(label='Plot') - self.plot_options_label.setToolTip( - "Plot (show) this object." - ) - grid0.addWidget(self.plot_cb, 0, 0) - - # Solid CB - self.solid_cb = FCCheckBox(label='Solid') - self.solid_cb.setToolTip( - "Solid color polygons." - ) - grid0.addWidget(self.solid_cb, 0, 1) - - # Multicolored CB - self.multicolored_cb = FCCheckBox(label='Multicolored') - self.multicolored_cb.setToolTip( - "Draw polygons in different colors." - ) - grid0.addWidget(self.multicolored_cb, 0, 2) - - ## Isolation Routing - self.isolation_routing_label = QtGui.QLabel("Isolation Routing:") - self.isolation_routing_label.setToolTip( - "Create a Geometry object with\n" - "toolpaths to cut outside polygons." - ) - self.custom_box.addWidget(self.isolation_routing_label) - - grid1 = QtGui.QGridLayout() - self.custom_box.addLayout(grid1) - tdlabel = QtGui.QLabel('Tool dia:') - tdlabel.setToolTip( - "Diameter of the cutting tool." - ) - grid1.addWidget(tdlabel, 0, 0) - self.iso_tool_dia_entry = LengthEntry() - grid1.addWidget(self.iso_tool_dia_entry, 0, 1) - - passlabel = QtGui.QLabel('Width (# passes):') - passlabel.setToolTip( - "Width of the isolation gap in\n" - "number (integer) of tool widths." - ) - grid1.addWidget(passlabel, 1, 0) - self.iso_width_entry = IntEntry() - grid1.addWidget(self.iso_width_entry, 1, 1) - - overlabel = QtGui.QLabel('Pass overlap:') - overlabel.setToolTip( - "How much (fraction of tool width)\n" - "to overlap each pass." - ) - grid1.addWidget(overlabel, 2, 0) - self.iso_overlap_entry = FloatEntry() - grid1.addWidget(self.iso_overlap_entry, 2, 1) - - # combine all passes CB - self.combine_passes_cb = FCCheckBox(label='Combine Passes') - self.combine_passes_cb.setToolTip( - "Combine all passes into one object" - ) - grid1.addWidget(self.combine_passes_cb, 3, 0) - - - self.generate_iso_button = QtGui.QPushButton('Generate Geometry') - self.generate_iso_button.setToolTip( - "Create the Geometry Object\n" - "for isolation routing." - ) - self.custom_box.addWidget(self.generate_iso_button) - - ## Board cuttout - self.board_cutout_label = QtGui.QLabel("Board cutout:") - self.board_cutout_label.setToolTip( - "Create toolpaths to cut around\n" - "the PCB and separate it from\n" - "the original board." - ) - self.custom_box.addWidget(self.board_cutout_label) - - grid2 = QtGui.QGridLayout() - self.custom_box.addLayout(grid2) - tdclabel = QtGui.QLabel('Tool dia:') - tdclabel.setToolTip( - "Diameter of the cutting tool." - ) - grid2.addWidget(tdclabel, 0, 0) - self.cutout_tooldia_entry = LengthEntry() - grid2.addWidget(self.cutout_tooldia_entry, 0, 1) - - marginlabel = QtGui.QLabel('Margin:') - marginlabel.setToolTip( - "Distance from objects at which\n" - "to draw the cutout." - ) - grid2.addWidget(marginlabel, 1, 0) - self.cutout_margin_entry = LengthEntry() - grid2.addWidget(self.cutout_margin_entry, 1, 1) - - gaplabel = QtGui.QLabel('Gap size:') - gaplabel.setToolTip( - "Size of the gaps in the toolpath\n" - "that will remain to hold the\n" - "board in place." - ) - grid2.addWidget(gaplabel, 2, 0) - self.cutout_gap_entry = LengthEntry() - grid2.addWidget(self.cutout_gap_entry, 2, 1) - - gapslabel = QtGui.QLabel('Gaps:') - gapslabel.setToolTip( - "Where to place the gaps, Top/Bottom\n" - "Left/Rigt, or on all 4 sides." - ) - grid2.addWidget(gapslabel, 3, 0) - self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'}, - {'label': '2 (L/R)', 'value': 'lr'}, - {'label': '4', 'value': '4'}]) - grid2.addWidget(self.gaps_radio, 3, 1) - - self.generate_cutout_button = QtGui.QPushButton('Generate Geometry') - self.generate_cutout_button.setToolTip( - "Generate the geometry for\n" - "the board cutout." - ) - self.custom_box.addWidget(self.generate_cutout_button) - - ## Non-copper regions - self.noncopper_label = QtGui.QLabel("Non-copper regions:") - self.noncopper_label.setToolTip( - "Create polygons covering the\n" - "areas without copper on the PCB.\n" - "Equivalent to the inverse of this\n" - "object. Can be used to remove all\n" - "copper from a specified region." - ) - self.custom_box.addWidget(self.noncopper_label) - - grid3 = QtGui.QGridLayout() - self.custom_box.addLayout(grid3) - - # Margin - bmlabel = QtGui.QLabel('Boundary Margin:') - bmlabel.setToolTip( - "Specify the edge of the PCB\n" - "by drawing a box around all\n" - "objects with this minimum\n" - "distance." - ) - grid3.addWidget(bmlabel, 0, 0) - self.noncopper_margin_entry = LengthEntry() - grid3.addWidget(self.noncopper_margin_entry, 0, 1) - - # Rounded corners - self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners") - self.noncopper_rounded_cb.setToolTip( - "Creates a Geometry objects with polygons\n" - "covering the copper-free areas of the PCB." - ) - grid3.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2) - - self.generate_noncopper_button = QtGui.QPushButton('Generate Geometry') - self.custom_box.addWidget(self.generate_noncopper_button) - - ## Bounding box - self.boundingbox_label = QtGui.QLabel('Bounding Box:') - self.custom_box.addWidget(self.boundingbox_label) - - grid4 = QtGui.QGridLayout() - self.custom_box.addLayout(grid4) - - bbmargin = QtGui.QLabel('Boundary Margin:') - bbmargin.setToolTip( - "Distance of the edges of the box\n" - "to the nearest polygon." - ) - grid4.addWidget(bbmargin, 0, 0) - self.bbmargin_entry = LengthEntry() - grid4.addWidget(self.bbmargin_entry, 0, 1) - - self.bbrounded_cb = FCCheckBox(label="Rounded corners") - self.bbrounded_cb.setToolTip( - "If the bounding box is \n" - "to have rounded corners\n" - "their radius is equal to\n" - "the margin." - ) - grid4.addWidget(self.bbrounded_cb, 1, 0, 1, 2) - - self.generate_bb_button = QtGui.QPushButton('Generate Geometry') - self.generate_bb_button.setToolTip( - "Generate the Geometry object." - ) - self.custom_box.addWidget(self.generate_bb_button) - - -# def main(): -# -# app = QtGui.QApplication(sys.argv) -# fc = GerberObjectUI() -# sys.exit(app.exec_()) -# -# -# if __name__ == '__main__': -# main() diff --git a/PlotCanvas.py b/PlotCanvas.py deleted file mode 100644 index 388c7fee..00000000 --- a/PlotCanvas.py +++ /dev/null @@ -1,526 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://caram.cl/software/flatcam # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -from PyQt4 import QtGui, QtCore - -# Prevent conflict with Qt5 and above. -from matplotlib import use as mpl_use -mpl_use("Qt4Agg") - -from matplotlib.figure import Figure -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas -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. - """ - - # 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) - - def __init__(self, container, app): - """ - The constructor configures the Matplotlib figure that - will contain all plots, creates the base axes and connects - events to the plotting area. - - :param container: The parent container in which to draw plots. - :rtype: PlotCanvas - """ - - super(PlotCanvas, self).__init__() - - self.app = app - - # Options - self.x_margin = 15 # pixels - self.y_margin = 25 # Pixels - - # Parent container - self.container = container - - # Plots go onto a single matplotlib.figure - self.figure = Figure(dpi=50) # TODO: dpi needed? - self.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) - self.axes.axhline(color='Black') - self.axes.axvline(color='Black') - - # The canvas is the top level container (FigureCanvasQTAgg) - self.canvas = FigureCanvas(self.figure) - # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) - # self.canvas.setFocus() - - #self.canvas.set_hexpand(1) - #self.canvas.set_vexpand(1) - #self.canvas.set_can_focus(True) # For key press - - # Attach to parent - #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? - self.container.addWidget(self.canvas) # Qt - - # Copy a bitmap of the canvas for quick animation. - # 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) - self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) - #self.canvas.connect('configure-event', self.auto_adjust_axes) - self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) - #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) - #self.canvas.connect("scroll-event", self.on_scroll) - self.canvas.mpl_connect('scroll_event', self.on_scroll) - self.canvas.mpl_connect('key_press_event', self.on_key_down) - self.canvas.mpl_connect('key_release_event', self.on_key_up) - self.canvas.mpl_connect('draw_event', self.on_draw) - - self.mouse = [0, 0] - self.key = None - - self.pan_axes = [] - self.panning = False - - def on_new_screen(self): - - log.debug("Cache updated the screen!") - - def on_key_down(self, event): - """ - - :param event: - :return: - """ - FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key)) - self.key = event.key - - def on_key_up(self, event): - """ - - :param event: - :return: - """ - self.key = None - - def mpl_connect(self, event_name, callback): - """ - Attach an event handler to the canvas through the Matplotlib interface. - - :param event_name: Name of the event - :type event_name: str - :param callback: Function to call - :type callback: func - :return: Connection id - :rtype: int - """ - return self.canvas.mpl_connect(event_name, callback) - - def mpl_disconnect(self, cid): - """ - Disconnect callback with the give id. - :param cid: Callback id. - :return: None - """ - self.canvas.mpl_disconnect(cid) - - def connect(self, event_name, callback): - """ - Attach an event handler to the canvas through the native Qt interface. - - :param event_name: Name of the event - :type event_name: str - :param callback: Function to call - :type callback: function - :return: Nothing - """ - self.canvas.connect(event_name, callback) - - def clear(self): - """ - Clears axes and figure. - - :return: None - """ - - # Clear - self.axes.cla() - try: - self.figure.clf() - except KeyError: - FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") - - # Re-build - self.figure.add_axes(self.axes) - self.axes.set_aspect(1) - self.axes.grid(True) - - # Re-draw - self.canvas.draw_idle() - - def adjust_axes(self, xmin, ymin, xmax, ymax): - """ - Adjusts all axes while maintaining the use of the whole canvas - and an aspect ratio to 1:1 between x and y axes. The parameters are an original - request that will be modified to fit these restrictions. - - :param xmin: Requested minimum value for the X axis. - :type xmin: float - :param ymin: Requested minimum value for the Y axis. - :type ymin: float - :param xmax: Requested maximum value for the X axis. - :type xmax: float - :param ymax: Requested maximum value for the Y axis. - :type ymax: float - :return: None - """ - - # FlatCAMApp.App.log.debug("PC.adjust_axes()") - - width = xmax - xmin - height = ymax - ymin - try: - r = width / height - except ZeroDivisionError: - FlatCAMApp.App.log.error("Height is %f" % height) - return - canvas_w, canvas_h = self.canvas.get_width_height() - canvas_r = float(canvas_w) / canvas_h - x_ratio = float(self.x_margin) / canvas_w - y_ratio = float(self.y_margin) / canvas_h - - if r > canvas_r: - ycenter = (ymin + ymax) / 2.0 - newheight = height * r / canvas_r - ymin = ycenter - newheight / 2.0 - ymax = ycenter + newheight / 2.0 - else: - xcenter = (xmax + xmin) / 2.0 - newwidth = width * canvas_r / r - xmin = xcenter - newwidth / 2.0 - xmax = xcenter + newwidth / 2.0 - - # Adjust axes - for ax in self.figure.get_axes(): - if ax._label != 'base': - ax.set_frame_on(False) # No frame - ax.set_xticks([]) # No tick - ax.set_yticks([]) # No ticks - ax.patch.set_visible(False) # No background - ax.set_aspect(1) - ax.set_xlim((xmin, xmax)) - ax.set_ylim((ymin, ymax)) - ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) - - # Sync re-draw to proper paint on form resize - self.canvas.draw() - - ##### Temporary place-holder for cached update ##### - self.update_screen_request.emit([0, 0, 0, 0, 0]) - - 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) - - def zoom(self, factor, center=None): - """ - Zooms the plot by factor around a given - center point. Takes care of re-drawing. - - :param factor: Number by which to scale the plot. - :type factor: float - :param center: Coordinates [x, y] of the point around which to scale the plot. - :type center: list - :return: None - """ - - xmin, xmax = self.axes.get_xlim() - ymin, ymax = self.axes.get_ylim() - width = xmax - xmin - height = ymax - ymin - - if center is None or center == [None, None]: - center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] - - # For keeping the point at the pointer location - relx = (xmax - center[0]) / width - rely = (ymax - center[1]) / height - - new_width = width / factor - new_height = height / factor - - xmin = center[0] - new_width * (1 - relx) - xmax = center[0] + new_width * relx - ymin = center[1] - new_height * (1 - rely) - ymax = center[1] + new_height * rely - - # Adjust axes - for ax in self.figure.get_axes(): - ax.set_xlim((xmin, xmax)) - ax.set_ylim((ymin, ymax)) - - # Async re-draw - self.canvas.draw_idle() - - ##### 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() - ymin, ymax = self.axes.get_ylim() - width = xmax - xmin - height = ymax - ymin - - # Adjust axes - for ax in self.figure.get_axes(): - ax.set_xlim((xmin + x * width, xmax + x * width)) - ax.set_ylim((ymin + y * height, ymax + y * height)) - - # 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. - - :param name: Unique label for the axes. - :return: Axes attached to the figure. - :rtype: Axes - """ - - return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) - - def on_scroll(self, event): - """ - Scroll event handler. - - :param event: Event object containing the event information. - :return: None - """ - - # So it can receive key presses - # self.canvas.grab_focus() - self.canvas.setFocus() - - # Event info - # z, direction = event.get_scroll_direction() - - if self.key is None: - - if event.button == 'up': - self.zoom(1.5, self.mouse) - else: - self.zoom(1 / 1.5, self.mouse) - return - - if self.key == 'shift': - - if event.button == 'up': - self.pan(0.3, 0) - else: - self.pan(-0.3, 0) - return - - if self.key == 'control': - - if event.button == 'up': - self.pan(0, 0.3) - else: - self.pan(0, -0.3) - return - - def on_mouse_press(self, event): - - # Check for middle mouse button press - if event.button == self.app.mouse_pan_button: - - # Prepare axes for pan (using 'matplotlib' pan function) - self.pan_axes = [] - for a in self.figure.get_axes(): - if (event.x is not None and event.y is not None and a.in_axes(event) and - a.get_navigate() and a.can_pan()): - a.start_pan(event.x, event.y, 1) - self.pan_axes.append(a) - - # Set pan view flag - if len(self.pan_axes) > 0: self.panning = True; - - def on_mouse_release(self, event): - - # Check for middle mouse button release to complete pan procedure - if event.button == self.app.mouse_pan_button: - for a in self.pan_axes: - a.end_pan() - - # Clear pan flag - self.panning = False - - def on_mouse_move(self, event): - """ - Mouse movement event hadler. Stores the coordinates. Updates view on pan. - - :param event: Contains information about the event. - :return: None - """ - self.mouse = [event.xdata, event.ydata] - - # Update pan view on mouse move - if self.panning is True: - for a in self.pan_axes: - a.drag_pan(1, event.key, event.x, event.y) - - # 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 deleted file mode 100644 index 3be39a38..00000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -FlatCAM: 2D Computer-Aided PCB Manufacturing -============================================ - -(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. diff --git a/ToolDblSided.py b/ToolDblSided.py deleted file mode 100644 index 4e52a4ba..00000000 --- a/ToolDblSided.py +++ /dev/null @@ -1,198 +0,0 @@ -from PyQt4 import QtGui -from GUIElements import RadioSet, EvalEntry, LengthEntry -from FlatCAMTool import FlatCAMTool -#from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon -from FlatCAMObj import * -from shapely.geometry import Point -from shapely import affinity - - -class DblSidedTool(FlatCAMTool): - - toolName = "Double-Sided PCB Tool" - - def __init__(self, app): - FlatCAMTool.__init__(self, app) - - ## Title - title_label = QtGui.QLabel("%s" % self.toolName) - self.layout.addWidget(title_label) - - ## Form Layout - form_layout = QtGui.QFormLayout() - self.layout.addLayout(form_layout) - - ## Layer to mirror - self.object_combo = QtGui.QComboBox() - self.object_combo.setModel(self.app.collection.model) - self.botlay_label = QtGui.QLabel("Bottom Layer:") - self.botlay_label.setToolTip( - "Layer to be mirrorer." - ) - # form_layout.addRow("Bottom Layer:", self.object_combo) - form_layout.addRow(self.botlay_label, self.object_combo) - - ## Axis - self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'}, - {'label': 'Y', 'value': 'Y'}]) - self.mirax_label = QtGui.QLabel("Mirror Axis:") - self.mirax_label.setToolTip( - "Mirror vertically (X) or horizontally (Y)." - ) - # form_layout.addRow("Mirror Axis:", self.mirror_axis) - form_layout.addRow(self.mirax_label, self.mirror_axis) - - ## Axis Location - self.axis_location = RadioSet([{'label': 'Point', 'value': 'point'}, - {'label': 'Box', 'value': 'box'}]) - self.axloc_label = QtGui.QLabel("Axis Location:") - self.axloc_label.setToolTip( - "The axis should pass through a point or cut " - "a specified box (in a Geometry object) in " - "the middle." - ) - # form_layout.addRow("Axis Location:", self.axis_location) - form_layout.addRow(self.axloc_label, self.axis_location) - - ## Point/Box - self.point_box_container = QtGui.QVBoxLayout() - self.pb_label = QtGui.QLabel("Point/Box:") - self.pb_label.setToolTip( - "Specify the point (x, y) through which the mirror axis " - "passes or the Geometry object containing a rectangle " - "that the mirror axis cuts in half." - ) - # form_layout.addRow("Point/Box:", self.point_box_container) - form_layout.addRow(self.pb_label, self.point_box_container) - - self.point = EvalEntry() - self.point_box_container.addWidget(self.point) - self.box_combo = QtGui.QComboBox() - self.box_combo.setModel(self.app.collection.model) - self.point_box_container.addWidget(self.box_combo) - self.box_combo.hide() - - ## Alignment holes - self.alignment_holes = EvalEntry() - self.ah_label = QtGui.QLabel("Alignment Holes:") - self.ah_label.setToolTip( - "Alignment holes (x1, y1), (x2, y2), ... " - "on one side of the mirror axis." - ) - form_layout.addRow(self.ah_label, self.alignment_holes) - - ## Drill diameter for alignment holes - self.drill_dia = LengthEntry() - self.dd_label = QtGui.QLabel("Drill diam.:") - self.dd_label.setToolTip( - "Diameter of the drill for the " - "alignment holes." - ) - form_layout.addRow(self.dd_label, self.drill_dia) - - ## Buttons - hlay = QtGui.QHBoxLayout() - self.layout.addLayout(hlay) - hlay.addStretch() - self.create_alignment_hole_button = QtGui.QPushButton("Create Alignment Drill") - self.create_alignment_hole_button.setToolTip( - "Creates an Excellon Object containing the " - "specified alignment holes and their mirror " - "images." - ) - self.mirror_object_button = QtGui.QPushButton("Mirror Object") - self.mirror_object_button.setToolTip( - "Mirrors (flips) the specified object around " - "the specified axis. Does not create a new " - "object, but modifies it." - ) - hlay.addWidget(self.create_alignment_hole_button) - hlay.addWidget(self.mirror_object_button) - - self.layout.addStretch() - - ## Signals - self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes) - self.mirror_object_button.clicked.connect(self.on_mirror) - - self.axis_location.group_toggle_fn = self.on_toggle_pointbox - - ## Initialize form - self.mirror_axis.set_value('X') - self.axis_location.set_value('point') - - def on_create_alignment_holes(self): - axis = self.mirror_axis.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - px, py = self.point.get_value() - else: - selection_index = self.box_combo.currentIndex() - bb_obj = self.app.collection.object_list[selection_index] # TODO: Direct access?? - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - dia = self.drill_dia.get_value() - tools = {"1": {"C": dia}} - - # holes = self.alignment_holes.get_value() - holes = eval('[{}]'.format(self.alignment_holes.text())) - drills = [] - - 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"}) - - def obj_init(obj_inst, app_inst): - obj_inst.tools = tools - obj_inst.drills = drills - obj_inst.create_geometry() - - self.app.new_object("excellon", "Alignment Drills", obj_init) - - def on_mirror(self): - selection_index = self.object_combo.currentIndex() - fcobj = self.app.collection.object_list[selection_index] - - # For now, lets limit to Gerbers and Excellons. - # assert isinstance(gerb, FlatCAMGerber) - if not isinstance(fcobj, FlatCAMGerber) and \ - not isinstance(fcobj, FlatCAMExcellon) and \ - not isinstance(fcobj, FlatCAMGeometry): - self.app.inform.emit("ERROR: Only Gerber, Excellon and Geometry objects can be mirrored.") - return - - axis = self.mirror_axis.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - px, py = self.point.get_value() - else: - selection_index = self.box_combo.currentIndex() - bb_obj = self.app.collection.object_list[selection_index] # TODO: Direct access?? - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - # Ensure that the selected object will display when it is mirrored. - # If an object's plot setting is False it will still be available in - # the combo box. If the plot is not enforced to True then the user - # gets no feedback of the operation. - fcobj.options["plot"] = True; - - fcobj.mirror(axis, [px, py]) - fcobj.plot() - - def on_toggle_pointbox(self): - if self.axis_location.get_value() == "point": - self.point.show() - self.box_combo.hide() - else: - self.point.hide() - self.box_combo.show() \ No newline at end of file diff --git a/ToolMeasurement.py b/ToolMeasurement.py deleted file mode 100644 index 2e2e76b1..00000000 --- a/ToolMeasurement.py +++ /dev/null @@ -1,74 +0,0 @@ -from PyQt4 import QtGui -from FlatCAMTool import FlatCAMTool -from copy import copy -from math import sqrt - - -class Measurement(FlatCAMTool): - - toolName = "Measurement Tool" - - def __init__(self, app): - FlatCAMTool.__init__(self, app) - - # self.setContentsMargins(0, 0, 0, 0) - self.layout.setMargin(0) - self.layout.setContentsMargins(0, 0, 3, 0) - - self.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Maximum) - - self.point1 = None - self.point2 = None - self.label = QtGui.QLabel("Click on a reference point ...") - self.label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) - self.label.setMargin(3) - self.layout.addWidget(self.label) - # self.layout.setMargin(0) - self.setVisible(False) - - self.click_subscription = None - self.move_subscription = None - - def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, **kwargs) - self.app.ui.right_layout.addWidget(self) - self.app.plotcanvas.mpl_connect('key_press_event', self.on_key_press) - - def run(self): - self.toggle() - - def on_click(self, event): - if self.point1 is None: - self.point1 = (event.xdata, event.ydata) - else: - self.point2 = copy(self.point1) - self.point1 = (event.xdata, event.ydata) - self.on_move(event) - - def on_key_press(self, event): - if event.key == 'r': - self.toggle() - - def toggle(self): - if self.isVisible(): - self.setVisible(False) - self.app.plotcanvas.mpl_disconnect(self.move_subscription) - self.app.plotcanvas.mpl_disconnect(self.click_subscription) - else: - self.setVisible(True) - self.move_subscription = self.app.plotcanvas.mpl_connect('motion_notify_event', self.on_move) - self.click_subscription = self.app.plotcanvas.mpl_connect('button_press_event', self.on_click) - - def on_move(self, event): - if self.point1 is None: - self.label.setText("Click on a reference point...") - else: - try: - dx = event.xdata - self.point1[0] - dy = event.ydata - self.point1[1] - d = sqrt(dx**2 + dy**2) - self.label.setText("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy)) - except TypeError: - pass - if self.update is not None: - self.update() diff --git a/ToolTransform.py b/ToolTransform.py deleted file mode 100644 index 65aa8247..00000000 --- a/ToolTransform.py +++ /dev/null @@ -1,319 +0,0 @@ -from PyQt4 import QtGui, QtCore -from PyQt4 import Qt -from GUIElements import FCEntry, FCButton -from FlatCAMTool import FlatCAMTool -from camlib import * - - -class ToolTransform(FlatCAMTool): - - toolName = "Object Transformation" - rotateName = "Rotate Transformation" - skewName = "Skew/Shear Transformation" - flipName = "Flip Transformation" - - def __init__(self, app): - FlatCAMTool.__init__(self, app) - - self.transform_lay = QtGui.QVBoxLayout() - self.layout.addLayout(self.transform_lay) - ## Title - title_label = QtGui.QLabel("%s
" % self.toolName) - self.transform_lay.addWidget(title_label) - - self.empty_label = QtGui.QLabel("") - self.empty_label.setFixedWidth(80) - self.empty_label1 = QtGui.QLabel("") - self.empty_label1.setFixedWidth(80) - self.empty_label2 = QtGui.QLabel("") - self.empty_label2.setFixedWidth(80) - self.transform_lay.addWidget(self.empty_label) - - ## Rotate Title - rotate_title_label = QtGui.QLabel("%s" % self.rotateName) - self.transform_lay.addWidget(rotate_title_label) - - ## Form Layout - form_layout = QtGui.QFormLayout() - self.transform_lay.addLayout(form_layout) - - self.rotate_entry = FCEntry() - self.rotate_entry.setFixedWidth(70) - self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.rotate_label = QtGui.QLabel("Angle Rotation:") - self.rotate_label.setToolTip( - "Angle for Rotation action, in degrees.\n" - "Float number between -360 and 359.\n" - "Positive numbers for CW motion.\n" - "Negative numbers for CCW motion." - ) - self.rotate_label.setFixedWidth(80) - - self.rotate_button = FCButton() - self.rotate_button.set_value("Rotate") - self.rotate_button.setToolTip( - "Rotate the selected object(s).\n" - "The point of reference is the middle of\n" - "the bounding box for all selected objects.\n" - ) - self.rotate_button.setFixedWidth(70) - - form_layout.addRow(self.rotate_label, self.rotate_entry) - form_layout.addRow(self.empty_label, self.rotate_button) - - self.transform_lay.addWidget(self.empty_label1) - - ## Skew Title - skew_title_label = QtGui.QLabel("%s" % self.skewName) - self.transform_lay.addWidget(skew_title_label) - - ## Form Layout - form1_layout = QtGui.QFormLayout() - self.transform_lay.addLayout(form1_layout) - - self.skewx_entry = FCEntry() - self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.skewx_entry.setFixedWidth(70) - self.skewx_label = QtGui.QLabel("Angle SkewX:") - self.skewx_label.setToolTip( - "Angle for Skew action, in degrees.\n" - "Float number between -360 and 359." - ) - self.skewx_label.setFixedWidth(80) - - self.skewx_button = FCButton() - self.skewx_button.set_value("Skew_X") - self.skewx_button.setToolTip( - "Skew/shear the selected object(s).\n" - "The point of reference is the middle of\n" - "the bounding box for all selected objects.\n") - self.skewx_button.setFixedWidth(70) - - self.skewy_entry = FCEntry() - self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.skewy_entry.setFixedWidth(70) - self.skewy_label = QtGui.QLabel("Angle SkewY:") - self.skewy_label.setToolTip( - "Angle for Skew action, in degrees.\n" - "Float number between -360 and 359." - ) - self.skewy_label.setFixedWidth(80) - - self.skewy_button = FCButton() - self.skewy_button.set_value("Skew_Y") - self.skewy_button.setToolTip( - "Skew/shear the selected object(s).\n" - "The point of reference is the middle of\n" - "the bounding box for all selected objects.\n") - self.skewy_button.setFixedWidth(70) - - form1_layout.addRow(self.skewx_label, self.skewx_entry) - form1_layout.addRow(self.empty_label, self.skewx_button) - form1_layout.addRow(self.skewy_label, self.skewy_entry) - form1_layout.addRow(self.empty_label, self.skewy_button) - - self.transform_lay.addWidget(self.empty_label2) - - ## Flip Title - flip_title_label = QtGui.QLabel("%s" % self.flipName) - self.transform_lay.addWidget(flip_title_label) - - ## Form Layout - form2_layout = QtGui.QFormLayout() - self.transform_lay.addLayout(form2_layout) - - self.flipx_button = FCButton() - self.flipx_button.set_value("Flip_X") - self.flipx_button.setToolTip( - "Flip the selected object(s) over the X axis.\n" - "Does not create a new object.\n " - ) - self.flipx_button.setFixedWidth(70) - - self.flipy_button = FCButton() - self.flipy_button.set_value("Flip_Y") - self.flipy_button.setToolTip( - "Flip the selected object(s) over the X axis.\n" - "Does not create a new object.\n " - ) - self.flipy_button.setFixedWidth(70) - - form2_layout.setSpacing(16) - form2_layout.addRow(self.flipx_button, self.flipy_button) - - self.transform_lay.addStretch() - - ## Signals - self.rotate_button.clicked.connect(self.on_rotate) - self.skewx_button.clicked.connect(self.on_skewx) - self.skewy_button.clicked.connect(self.on_skewy) - self.flipx_button.clicked.connect(self.on_flipx) - self.flipy_button.clicked.connect(self.on_flipy) - - self.rotate_entry.returnPressed.connect(self.on_rotate) - self.skewx_entry.returnPressed.connect(self.on_skewx) - self.skewy_entry.returnPressed.connect(self.on_skewy) - - ## Initialize form - self.rotate_entry.set_value('0') - self.skewx_entry.set_value('0') - self.skewy_entry.set_value('0') - - def on_rotate(self): - value = float(self.rotate_entry.get_value()) - self.on_rotate_action(value) - return - - def on_flipx(self): - self.on_flip("Y") - return - - def on_flipy(self): - self.on_flip("X") - return - - def on_skewx(self): - value = float(self.skewx_entry.get_value()) - self.on_skew("X", value) - return - - def on_skewy(self): - value = float(self.skewy_entry.get_value()) - self.on_skew("Y", value) - return - - def on_rotate_action(self, num): - obj_list = self.app.collection.get_selected() - xminlist = [] - yminlist = [] - xmaxlist = [] - ymaxlist = [] - - if not obj_list: - self.app.inform.emit("WARNING: No object selected.") - msg = "Please Select an object to rotate!" - warningbox = QtGui.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtGui.QMessageBox.Ok) - warningbox.setDefaultButton(QtGui.QMessageBox.Ok) - warningbox.exec_() - else: - try: - # first get a bounding box to fit all - for obj in obj_list: - xmin, ymin, xmax, ymax = obj.bounds() - xminlist.append(xmin) - yminlist.append(ymin) - xmaxlist.append(xmax) - ymaxlist.append(ymax) - - # get the minimum x,y and maximum x,y for all objects selected - xminimal = min(xminlist) - yminimal = min(yminlist) - xmaximal = max(xmaxlist) - ymaximal = max(ymaxlist) - - for sel_obj in obj_list: - px = 0.5 * (xminimal + xmaximal) - py = 0.5 * (yminimal + ymaximal) - - sel_obj.rotate(-num, point=(px, py)) - sel_obj.plot() - self.app.inform.emit('Object was rotated ...') - except Exception as e: - self.app.inform.emit("[ERROR] Due of %s, rotation movement was not executed." % str(e)) - raise - - def on_flip(self, axis): - obj_list = self.app.collection.get_selected() - xminlist = [] - yminlist = [] - xmaxlist = [] - ymaxlist = [] - - if not obj_list: - self.app.inform.emit("WARNING: No object selected.") - msg = "Please Select an object to flip!" - warningbox = QtGui.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtGui.QMessageBox.Ok) - warningbox.setDefaultButton(QtGui.QMessageBox.Ok) - warningbox.exec_() - return - else: - try: - # first get a bounding box to fit all - for obj in obj_list: - xmin, ymin, xmax, ymax = obj.bounds() - xminlist.append(xmin) - yminlist.append(ymin) - xmaxlist.append(xmax) - ymaxlist.append(ymax) - - # get the minimum x,y and maximum x,y for all objects selected - xminimal = min(xminlist) - yminimal = min(yminlist) - xmaximal = max(xmaxlist) - ymaximal = max(ymaxlist) - - px = 0.5 * (xminimal + xmaximal) - py = 0.5 * (yminimal + ymaximal) - - # execute mirroring - for obj in obj_list: - if axis is 'X': - obj.mirror('X', [px, py]) - obj.plot() - self.app.inform.emit('Flipped on the Y axis ...') - elif axis is 'Y': - obj.mirror('Y', [px, py]) - obj.plot() - self.app.inform.emit('Flipped on the X axis ...') - - except Exception as e: - self.app.inform.emit("[ERROR] Due of %s, Flip action was not executed.") - raise - - def on_skew(self, axis, num): - obj_list = self.app.collection.get_selected() - xminlist = [] - yminlist = [] - - if not obj_list: - self.app.inform.emit("WARNING: No object selected.") - msg = "Please Select an object to skew/shear!" - warningbox = QtGui.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtGui.QMessageBox.Ok) - warningbox.setDefaultButton(QtGui.QMessageBox.Ok) - warningbox.exec_() - else: - try: - # first get a bounding box to fit all - for obj in obj_list: - xmin, ymin, xmax, ymax = obj.bounds() - xminlist.append(xmin) - yminlist.append(ymin) - - # get the minimum x,y and maximum x,y for all objects selected - xminimal = min(xminlist) - yminimal = min(yminlist) - - for obj in obj_list: - if axis is 'X': - obj.skew(num, 0, point=(xminimal, yminimal)) - elif axis is 'Y': - obj.skew(0, num, point=(xminimal, yminimal)) - obj.plot() - self.app.inform.emit('Object was skewed on %s axis ...' % str(axis)) - except Exception as e: - self.app.inform.emit("[ERROR] Due of %s, Skew action was not executed." % str(e)) - raise - -# end of file \ No newline at end of file diff --git a/camlib.py b/camlib.py deleted file mode 100644 index b41b08a7..00000000 --- a/camlib.py +++ /dev/null @@ -1,4423 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ -#from __future__ import division -#from scipy import optimize -#import traceback - -from io import StringIO -from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \ - transpose -from numpy.linalg import solve, norm -from matplotlib.figure import Figure -import re -import sys -import traceback -from decimal import Decimal - -import collections -import numpy as np -import matplotlib -#import matplotlib.pyplot as plt -#from scipy.spatial import Delaunay, KDTree - -from rtree import index as rtindex - -# See: http://toblerity.org/shapely/manual.html -from shapely.geometry import Polygon, LineString, Point, LinearRing -from shapely.geometry import MultiPoint, MultiPolygon -from shapely.geometry import box as shply_box -from shapely.ops import cascaded_union, unary_union -import shapely.affinity as affinity -from shapely.wkt import loads as sloads -from shapely.wkt import dumps as sdumps -from shapely.geometry.base import BaseGeometry - -# Used for solid polygons in Matplotlib -from descartes.patch import PolygonPatch - -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') -log.setLevel(logging.DEBUG) -# log.setLevel(logging.WARNING) -# log.setLevel(logging.INFO) -formatter = logging.Formatter('[%(levelname)s] %(message)s') -handler = logging.StreamHandler() -handler.setFormatter(formatter) -log.addHandler(handler) - - -class ParseError(Exception): - pass - - -class Geometry(object): - """ - Base geometry class. - """ - - defaults = { - "init_units": 'in' - } - - def __init__(self): - # Units (in or mm) - self.units = Geometry.defaults["init_units"] - - # Final geometry: MultiPolygon or list (of geometry constructs) - self.solid_geometry = None - - # Attributes to be included in serialization - self.ser_attrs = ['units', 'solid_geometry'] - - # Flattened geometry (list of paths only) - self.flat_geometry = [] - - # Index - self.index = None - - def make_index(self): - self.flatten() - self.index = FlatCAMRTree() - - for i, g in enumerate(self.flat_geometry): - self.index.insert(i, g) - - def add_circle(self, origin, radius): - """ - Adds a circle to the object. - - :param origin: Center of the circle. - :param radius: Radius of the circle. - :return: None - """ - # TODO: Decide what solid_geometry is supposed to be and how we append to it. - - if self.solid_geometry is None: - self.solid_geometry = [] - - if type(self.solid_geometry) is list: - self.solid_geometry.append(Point(origin).buffer(radius)) - return - - try: - self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(radius)) - except: - #print "Failed to run union on polygons." - log.error("Failed to run union on polygons.") - raise - - def add_polygon(self, points): - """ - Adds a polygon to the object (by union) - - :param points: The vertices of the polygon. - :return: None - """ - if self.solid_geometry is None: - self.solid_geometry = [] - - if type(self.solid_geometry) is list: - self.solid_geometry.append(Polygon(points)) - return - - try: - self.solid_geometry = self.solid_geometry.union(Polygon(points)) - except: - #print "Failed to run union on polygons." - 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 - of geometry: (xmin, ymin, xmax, ymax). - """ - log.debug("Geometry->bounds()") - if self.solid_geometry is None: - log.debug("solid_geometry is None") - return 0, 0, 0, 0 - - if type(self.solid_geometry) is list: - # TODO: This can be done faster. See comment from Shapely mailing lists. - if len(self.solid_geometry) == 0: - log.debug('solid_geometry is empty []') - return 0, 0, 0, 0 - return cascaded_union(self.solid_geometry).bounds - else: - return self.solid_geometry.bounds - - def find_polygon(self, point, geoset=None): - """ - Find an object that object.contains(Point(point)) in - geoset, which can can be iterable, contain iterables of, or - be itself an implementer of .contains(). - - Note: - * Shapely Polygons will work as expected here. Linearrings - will only yield true if the point is in the perimeter. - - :param point: See description - :param geoset: Set to search. If none, the defaults to - self.solid_geometry. - :return: Polygon containing point or None. - """ - - if geoset is None: - geoset = self.solid_geometry - - try: # Iterable - for sub_geo in geoset: - p = self.find_polygon(point, geoset=sub_geo) - if p is not None: - return p - - except TypeError: # Non-iterable - - try: # Implements .contains() - if geoset.contains(Point(point)): - return geoset - - except AttributeError: # Does not implement .contains() - return None - - return None - - def get_interiors(self, geometry=None): - - interiors = [] - - if geometry is None: - geometry = self.solid_geometry - - ## If iterable, expand recursively. - try: - for geo in geometry: - interiors.extend(self.get_interiors(geometry=geo)) - - ## Not iterable, get the exterior if polygon. - except TypeError: - if type(geometry) == Polygon: - interiors.extend(geometry.interiors) - - return interiors - - def get_exteriors(self, geometry=None): - """ - Returns all exteriors of polygons in geometry. Uses - ``self.solid_geometry`` if geometry is not provided. - - :param geometry: Shapely type or list or list of list of such. - :return: List of paths constituting the exteriors - of polygons in geometry. - """ - - exteriors = [] - - if geometry is None: - geometry = self.solid_geometry - - ## If iterable, expand recursively. - try: - for geo in geometry: - exteriors.extend(self.get_exteriors(geometry=geo)) - - ## Not iterable, get the exterior if polygon. - except TypeError: - if type(geometry) == Polygon: - exteriors.append(geometry.exterior) - - return exteriors - - def flatten(self, geometry=None, reset=True, pathonly=False): - """ - Creates a list of non-iterable linear geometry objects. - Polygons are expanded into its exterior and interiors if specified. - - Results are placed in self.flat_geoemtry - - :param geometry: Shapely type or list or list of list of such. - :param reset: Clears the contents of self.flat_geometry. - :param pathonly: Expands polygons into linear elements. - """ - - if geometry is None: - geometry = self.solid_geometry - - if reset: - self.flat_geometry = [] - - ## If iterable, expand recursively. - try: - for geo in geometry: - self.flatten(geometry=geo, - reset=False, - pathonly=pathonly) - - ## Not iterable, do the actual indexing and add. - except TypeError: - if pathonly and type(geometry) == Polygon: - self.flat_geometry.append(geometry.exterior) - self.flatten(geometry=geometry.interiors, - reset=False, - pathonly=True) - else: - self.flat_geometry.append(geometry) - - return self.flat_geometry - - # def make2Dstorage(self): - # - # self.flatten() - # - # def get_pts(o): - # pts = [] - # if type(o) == Polygon: - # g = o.exterior - # pts += list(g.coords) - # for i in o.interiors: - # pts += list(i.coords) - # else: - # pts += list(o.coords) - # return pts - # - # storage = FlatCAMRTreeStorage() - # storage.get_points = get_pts - # for shape in self.flat_geometry: - # storage.insert(shape) - # return storage - - # def flatten_to_paths(self, geometry=None, reset=True): - # """ - # Creates a list of non-iterable linear geometry elements and - # indexes them in rtree. - # - # :param geometry: Iterable geometry - # :param reset: Wether to clear (True) or append (False) to self.flat_geometry - # :return: self.flat_geometry, self.flat_geometry_rtree - # """ - # - # if geometry is None: - # geometry = self.solid_geometry - # - # if reset: - # self.flat_geometry = [] - # - # ## If iterable, expand recursively. - # try: - # for geo in geometry: - # self.flatten_to_paths(geometry=geo, reset=False) - # - # ## Not iterable, do the actual indexing and add. - # except TypeError: - # if type(geometry) == Polygon: - # g = geometry.exterior - # self.flat_geometry.append(g) - # - # ## Add first and last points of the path to the index. - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0]) - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1]) - # - # for interior in geometry.interiors: - # g = interior - # self.flat_geometry.append(g) - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0]) - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1]) - # else: - # g = geometry - # self.flat_geometry.append(g) - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0]) - # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1]) - # - # return self.flat_geometry, self.flat_geometry_rtree - - def isolation_geometry(self, offset): - """ - Creates contours around geometry at a given - offset distance. - - :param offset: Offset distance. - :type offset: float - :return: The buffered geometry. - :rtype: Shapely.MultiPolygon or Shapely.Polygon - """ - return self.solid_geometry.buffer(offset) - - 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 - :param flip: Flip the vertically. - :type flip: bool - :return: None - """ - - # Parse into list of shapely objects - svg_tree = ET.parse(filename) - svg_root = svg_tree.getroot() - - # Change origin to bottom left - 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: - self.solid_geometry = [] - - if type(self.solid_geometry) is list: - # self.solid_geometry.append(cascaded_union(geos)) - if type(geos) is list: - self.solid_geometry += geos - else: - self.solid_geometry.append(geos) - else: # It's shapely geometry - # self.solid_geometry = cascaded_union([self.solid_geometry, - # cascaded_union(geos)]) - self.solid_geometry = [self.solid_geometry, geos] - - def size(self): - """ - Returns (width, height) of rectangular - bounds of geometry. - """ - if self.solid_geometry is None: - log.warning("Solid_geometry not computed yet.") - return 0 - bounds = self.bounds() - return bounds[2] - bounds[0], bounds[3] - bounds[1] - - def get_empty_area(self, boundary=None): - """ - Returns the complement of self.solid_geometry within - the given boundary polygon. If not specified, it defaults to - the rectangular bounding box of self.solid_geometry. - """ - if boundary is None: - boundary = self.solid_geometry.envelope - return boundary.difference(self.solid_geometry) - - @staticmethod - def clear_polygon(polygon, tooldia, overlap=0.15, connect=True, - contour=True): - """ - Creates geometry inside a polygon for a tool to cover - the whole area. - - This algorithm shrinks the edges of the polygon and takes - the resulting edges as toolpaths. - - :param polygon: Polygon to clear. - :param tooldia: Diameter of the tool. - :param overlap: Overlap of toolpasses. - :param connect: Draw lines between disjoint segments to - minimize tool lifts. - :param contour: Paint around the edges. Inconsequential in - this painting method. - :return: - """ - - log.debug("camlib.clear_polygon()") - assert type(polygon) == Polygon or type(polygon) == MultiPolygon, \ - "Expected a Polygon or MultiPolygon, got %s" % type(polygon) - - ## The toolpaths - # Index first and last points in paths - def get_pts(o): - return [o.coords[0], o.coords[-1]] - geoms = FlatCAMRTreeStorage() - geoms.get_points = get_pts - - # Can only result in a Polygon or MultiPolygon - # NOTE: The resulting polygon can be "empty". - current = polygon.buffer(-tooldia / 2.0) - if current.area == 0: - # Otherwise, trying to to insert current.exterior == None - # into the FlatCAMStorage will fail. - return None - - # current can be a MultiPolygon - try: - for p in current: - geoms.insert(p.exterior) - for i in p.interiors: - geoms.insert(i) - - # Not a Multipolygon. Must be a Polygon - except TypeError: - geoms.insert(current.exterior) - for i in current.interiors: - geoms.insert(i) - - while True: - - # Can only result in a Polygon or MultiPolygon - current = current.buffer(-tooldia * (1 - overlap)) - if current.area > 0: - - # current can be a MultiPolygon - try: - for p in current: - geoms.insert(p.exterior) - for i in p.interiors: - geoms.insert(i) - - # Not a Multipolygon. Must be a Polygon - except TypeError: - geoms.insert(current.exterior) - for i in current.interiors: - geoms.insert(i) - else: - break - - # Optimization: Reduce lifts - if connect: - log.debug("Reducing tool lifts...") - geoms = Geometry.paint_connect(geoms, polygon, tooldia) - - return geoms - - @staticmethod - def clear_polygon2(polygon, tooldia, seedpoint=None, overlap=0.15, - connect=True, contour=True): - """ - Creates geometry inside a polygon for a tool to cover - the whole area. - - This algorithm starts with a seed point inside the polygon - and draws circles around it. Arcs inside the polygons are - valid cuts. Finalizes by cutting around the inside edge of - the polygon. - - :param polygon: Shapely.geometry.Polygon - :param tooldia: Diameter of the tool - :param seedpoint: Shapely.geometry.Point or None - :param overlap: Tool fraction overlap bewteen passes - :param connect: Connect disjoint segment to minumize tool lifts - :param contour: Cut countour inside the polygon. - :return: List of toolpaths covering polygon. - :rtype: FlatCAMRTreeStorage | None - """ - - log.debug("camlib.clear_polygon2()") - - # Current buffer radius - radius = tooldia / 2 * (1 - overlap) - - ## The toolpaths - # Index first and last points in paths - def get_pts(o): - return [o.coords[0], o.coords[-1]] - geoms = FlatCAMRTreeStorage() - geoms.get_points = get_pts - - # Path margin - path_margin = polygon.buffer(-tooldia / 2) - - # Estimate good seedpoint if not provided. - if seedpoint is None: - seedpoint = path_margin.representative_point() - - # Grow from seed until outside the box. The polygons will - # never have an interior, so take the exterior LinearRing. - while 1: - path = Point(seedpoint).buffer(radius).exterior - path = path.intersection(path_margin) - - # Touches polygon? - if path.is_empty: - break - else: - #geoms.append(path) - #geoms.insert(path) - # path can be a collection of paths. - try: - for p in path: - geoms.insert(p) - except TypeError: - geoms.insert(path) - - radius += tooldia * (1 - overlap) - - # Clean inside edges (contours) of the original polygon - if contour: - outer_edges = [x.exterior for x in autolist(polygon.buffer(-tooldia / 2))] - inner_edges = [] - for x in autolist(polygon.buffer(-tooldia / 2)): # Over resulting polygons - for y in x.interiors: # Over interiors of each polygon - inner_edges.append(y) - #geoms += outer_edges + inner_edges - for g in outer_edges + inner_edges: - geoms.insert(g) - - # Optimization connect touching paths - # log.debug("Connecting paths...") - # geoms = Geometry.path_connect(geoms) - - # Optimization: Reduce lifts - if connect: - log.debug("Reducing tool lifts...") - geoms = Geometry.paint_connect(geoms, polygon, tooldia) - - return geoms - - @staticmethod - def clear_polygon3(polygon, tooldia, overlap=0.15, connect=True, - contour=True): - """ - Creates geometry inside a polygon for a tool to cover - the whole area. - - This algorithm draws horizontal lines inside the polygon. - - :param polygon: The polygon being painted. - :type polygon: shapely.geometry.Polygon - :param tooldia: Tool diameter. - :param overlap: Tool path overlap percentage. - :param connect: Connect lines to avoid tool lifts. - :param contour: Paint around the edges. - :return: - """ - - log.debug("camlib.clear_polygon3()") - - ## The toolpaths - # Index first and last points in paths - def get_pts(o): - return [o.coords[0], o.coords[-1]] - - geoms = FlatCAMRTreeStorage() - geoms.get_points = get_pts - - lines = [] - - # Bounding box - left, bot, right, top = polygon.bounds - - # First line - y = top - tooldia / 2 - while y > bot + tooldia / 2: - line = LineString([(left, y), (right, y)]) - lines.append(line) - y -= tooldia * (1 - overlap) - - # Last line - y = bot + tooldia / 2 - line = LineString([(left, y), (right, y)]) - lines.append(line) - - # Combine - linesgeo = unary_union(lines) - - # Trim to the polygon - margin_poly = polygon.buffer(-tooldia / 2) - lines_trimmed = linesgeo.intersection(margin_poly) - - # Add lines to storage - for line in lines_trimmed: - geoms.insert(line) - - # Add margin (contour) to storage - if contour: - geoms.insert(margin_poly.exterior) - for ints in margin_poly.interiors: - geoms.insert(ints) - - # Optimization: Reduce lifts - if connect: - log.debug("Reducing tool lifts...") - geoms = Geometry.paint_connect(geoms, polygon, tooldia) - - return geoms - - def scale(self, factor): - """ - Scales all of the object's geometry by a given factor. Override - this method. - :param factor: Number by which to scale. - :type factor: float - :return: None - :rtype: None - """ - return - - def offset(self, vect): - """ - Offset the geometry by the given vector. Override this method. - - :param vect: (x, y) vector by which to offset the object. - :type vect: tuple - :return: None - """ - return - - @staticmethod - def paint_connect(storage, boundary, tooldia, max_walk=None): - """ - Connects paths that results in a connection segment that is - within the paint area. This avoids unnecessary tool lifting. - - :param storage: Geometry to be optimized. - :type storage: FlatCAMRTreeStorage - :param boundary: Polygon defining the limits of the paintable area. - :type boundary: Polygon - :param tooldia: Tool diameter. - :rtype tooldia: float - :param max_walk: Maximum allowable distance without lifting tool. - :type max_walk: float or None - :return: Optimized geometry. - :rtype: FlatCAMRTreeStorage - """ - - # If max_walk is not specified, the maximum allowed is - # 10 times the tool diameter - max_walk = max_walk or 10 * tooldia - - # Assuming geolist is a flat list of flat elements - - ## Index first and last points in paths - def get_pts(o): - return [o.coords[0], o.coords[-1]] - - # storage = FlatCAMRTreeStorage() - # storage.get_points = get_pts - # - # for shape in geolist: - # if shape is not None: # TODO: This shouldn't have happened. - # # Make LlinearRings into linestrings otherwise - # # When chaining the coordinates path is messed up. - # storage.insert(LineString(shape)) - # #storage.insert(shape) - - ## Iterate over geometry paths getting the nearest each time. - #optimized_paths = [] - optimized_paths = FlatCAMRTreeStorage() - optimized_paths.get_points = get_pts - path_count = 0 - current_pt = (0, 0) - pt, geo = storage.nearest(current_pt) - storage.remove(geo) - geo = LineString(geo) - current_pt = geo.coords[-1] - try: - while True: - path_count += 1 - #log.debug("Path %d" % path_count) - - pt, candidate = storage.nearest(current_pt) - storage.remove(candidate) - candidate = LineString(candidate) - - # If last point in geometry is the nearest - # then reverse coordinates. - # but prefer the first one if last == first - if pt != candidate.coords[0] and pt == candidate.coords[-1]: - candidate.coords = list(candidate.coords)[::-1] - - # Straight line from current_pt to pt. - # Is the toolpath inside the geometry? - walk_path = LineString([current_pt, pt]) - walk_cut = walk_path.buffer(tooldia / 2) - - if walk_cut.within(boundary) and walk_path.length < max_walk: - #log.debug("Walk to path #%d is inside. Joining." % path_count) - - # Completely inside. Append... - geo.coords = list(geo.coords) + list(candidate.coords) - # try: - # last = optimized_paths[-1] - # last.coords = list(last.coords) + list(geo.coords) - # except IndexError: - # optimized_paths.append(geo) - - else: - - # Have to lift tool. End path. - #log.debug("Path #%d not within boundary. Next." % path_count) - #optimized_paths.append(geo) - optimized_paths.insert(geo) - geo = candidate - - current_pt = geo.coords[-1] - - # Next - #pt, geo = storage.nearest(current_pt) - - except StopIteration: # Nothing left in storage. - #pass - optimized_paths.insert(geo) - - return optimized_paths - - @staticmethod - def path_connect(storage, origin=(0, 0)): - """ - Simplifies paths in the FlatCAMRTreeStorage storage by - connecting paths that touch on their enpoints. - - :param storage: Storage containing the initial paths. - :rtype storage: FlatCAMRTreeStorage - :return: Simplified storage. - :rtype: FlatCAMRTreeStorage - """ - - log.debug("path_connect()") - - ## Index first and last points in paths - def get_pts(o): - return [o.coords[0], o.coords[-1]] - # - # storage = FlatCAMRTreeStorage() - # storage.get_points = get_pts - # - # for shape in pathlist: - # if shape is not None: # TODO: This shouldn't have happened. - # storage.insert(shape) - - path_count = 0 - pt, geo = storage.nearest(origin) - storage.remove(geo) - #optimized_geometry = [geo] - optimized_geometry = FlatCAMRTreeStorage() - optimized_geometry.get_points = get_pts - #optimized_geometry.insert(geo) - try: - while True: - path_count += 1 - - #print "geo is", geo - - _, left = storage.nearest(geo.coords[0]) - #print "left is", left - - # If left touches geo, remove left from original - # storage and append to geo. - if type(left) == LineString: - if left.coords[0] == geo.coords[0]: - storage.remove(left) - geo.coords = list(geo.coords)[::-1] + list(left.coords) - continue - - if left.coords[-1] == geo.coords[0]: - storage.remove(left) - geo.coords = list(left.coords) + list(geo.coords) - continue - - if left.coords[0] == geo.coords[-1]: - storage.remove(left) - geo.coords = list(geo.coords) + list(left.coords) - continue - - if left.coords[-1] == geo.coords[-1]: - storage.remove(left) - geo.coords = list(geo.coords) + list(left.coords)[::-1] - continue - - _, right = storage.nearest(geo.coords[-1]) - #print "right is", right - - # If right touches geo, remove left from original - # storage and append to geo. - if type(right) == LineString: - if right.coords[0] == geo.coords[-1]: - storage.remove(right) - geo.coords = list(geo.coords) + list(right.coords) - continue - - if right.coords[-1] == geo.coords[-1]: - storage.remove(right) - geo.coords = list(geo.coords) + list(right.coords)[::-1] - continue - - if right.coords[0] == geo.coords[0]: - storage.remove(right) - geo.coords = list(geo.coords)[::-1] + list(right.coords) - continue - - if right.coords[-1] == geo.coords[0]: - storage.remove(right) - geo.coords = list(left.coords) + list(geo.coords) - continue - - # right is either a LinearRing or it does not connect - # to geo (nothing left to connect to geo), so we continue - # with right as geo. - storage.remove(right) - - if type(right) == LinearRing: - optimized_geometry.insert(right) - else: - # Cannot exteng geo any further. Put it away. - optimized_geometry.insert(geo) - - # Continue with right. - geo = right - - except StopIteration: # Nothing found in storage. - optimized_geometry.insert(geo) - - #print path_count - log.debug("path_count = %d" % path_count) - - return optimized_geometry - - def convert_units(self, units): - """ - Converts the units of the object to ``units`` by scaling all - the geometry appropriately. This call ``scale()``. Don't call - it again in descendents. - - :param units: "IN" or "MM" - :type units: str - :return: Scaling factor resulting from unit change. - :rtype: float - """ - log.debug("Geometry.convert_units()") - - if units.upper() == self.units.upper(): - return 1.0 - - if units.upper() == "MM": - factor = 25.4 - elif units.upper() == "IN": - factor = 1 / 25.4 - else: - log.error("Unsupported units: %s" % str(units)) - return 1.0 - - self.units = units - self.scale(factor) - return factor - - def to_dict(self): - """ - Returns a respresentation of the object as a dictionary. - Attributes to include are listed in ``self.ser_attrs``. - - :return: A dictionary-encoded copy of the object. - :rtype: dict - """ - d = {} - for attr in self.ser_attrs: - d[attr] = getattr(self, attr) - return d - - def from_dict(self, d): - """ - Sets object's attributes from a dictionary. - Attributes to include are listed in ``self.ser_attrs``. - This method will look only for only and all the - attributes in ``self.ser_attrs``. They must all - be present. Use only for deserializing saved - objects. - - :param d: Dictionary of attributes to set in the object. - :type d: dict - :return: None - """ - for attr in self.ser_attrs: - setattr(self, attr, d[attr]) - - def union(self): - """ - Runs a cascaded union on the list of objects in - solid_geometry. - - :return: None - """ - self.solid_geometry = [cascaded_union(self.solid_geometry)] - - def export_svg(self, scale_factor=0.00): - """ - Exports the Geometry 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] - def mirror_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(mirror_geom(g)) - return new_obj - else: - return affinity.scale(obj, xscale, yscale, origin=(px,py)) - - self.solid_geometry = mirror_geom(self.solid_geometry) - - def skew(self, angle_x=None, angle_y=None, point=None): - """ - Shear/Skew the geometries of an object by angles along x and y dimensions. - - Parameters - ---------- - xs, ys : float, float - The shear angle(s) for the x and y axes respectively. These can be - specified in either degrees (default) or radians by setting - use_radians=True. - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - - if angle_x is None: - angle_x = 0 - if angle_y is None: - angle_y = 0 - if point is None: - point = (0,0) - else: - px, py = point - - def skew_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(skew_geom(g)) - return new_obj - else: - return affinity.skew(obj, angle_x, angle_y, - origin=(px, py)) - - self.solid_geometry = skew_geom(self.solid_geometry) - return - - def rotate(self, angle, point=None): - """ - Rotate an object by an angle (in degrees) around the provided coordinates. - - Parameters - ---------- - The angle of rotation are specified in degrees (default). Positive angles are - counter-clockwise and negative are clockwise rotations. - - The point of origin can be a keyword 'center' for the bounding box - center (default), 'centroid' for the geometry's centroid, a Point object - or a coordinate tuple (x0, y0). - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - if point is not None: - px, py = point - else: - px, py = (0,0) - - def rotate_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(rotate_geom(g)) - return new_obj - else: - return affinity.rotate(obj, angle, origin=(px, py)) - - self.solid_geometry = rotate_geom(self.solid_geometry) - return - -class ApertureMacro: - """ - Syntax of aperture macros. - - : AM* - : {{*}{*}} - : $K= - : ,{,}| - : $M|< Arithmetic expression> - : 0 - """ - - ## Regular expressions - am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$') - am2_re = re.compile(r'(.*)%$') - amcomm_re = re.compile(r'^0(.*)') - amprim_re = re.compile(r'^[1-9].*') - amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)') - - def __init__(self, name=None): - self.name = name - self.raw = "" - - ## These below are recomputed for every aperture - ## definition, in other words, are temporary variables. - self.primitives = [] - self.locvars = {} - self.geometry = None - - def to_dict(self): - """ - Returns the object in a serializable form. Only the name and - raw are required. - - :return: Dictionary representing the object. JSON ready. - :rtype: dict - """ - - return { - 'name': self.name, - 'raw': self.raw - } - - def from_dict(self, d): - """ - Populates the object from a serial representation created - with ``self.to_dict()``. - - :param d: Serial representation of an ApertureMacro object. - :return: None - """ - for attr in ['name', 'raw']: - setattr(self, attr, d[attr]) - - def parse_content(self): - """ - Creates numerical lists for all primitives in the aperture - macro (in ``self.raw``) by replacing all variables by their - values iteratively and evaluating expressions. Results - are stored in ``self.primitives``. - - :return: None - """ - # Cleanup - self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *") - self.primitives = [] - - # Separate parts - parts = self.raw.split('*') - - #### Every part in the macro #### - for part in parts: - ### Comments. Ignored. - match = ApertureMacro.amcomm_re.search(part) - if match: - continue - - ### Variables - # These are variables defined locally inside the macro. They can be - # numerical constant or defind in terms of previously define - # variables, which can be defined locally or in an aperture - # definition. All replacements ocurr here. - match = ApertureMacro.amvar_re.search(part) - if match: - var = match.group(1) - val = match.group(2) - - # Replace variables in value - for v in self.locvars: - val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val) - - # Make all others 0 - val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val) - - # Change x with * - val = re.sub(r'[xX]', "*", val) - - # Eval() and store. - self.locvars[var] = eval(val) - continue - - ### Primitives - # Each is an array. The first identifies the primitive, while the - # rest depend on the primitive. All are strings representing a - # number and may contain variable definition. The values of these - # variables are defined in an aperture definition. - match = ApertureMacro.amprim_re.search(part) - if match: - ## Replace all variables - for v in self.locvars: - part = re.sub(r'\$' + str(v) + r'(?![0-9a-zA-Z])', str(self.locvars[v]), part) - - # Make all others 0 - part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part) - - # Change x with * - part = re.sub(r'[xX]', "*", part) - - ## Store - elements = part.split(",") - self.primitives.append([eval(x) for x in elements]) - continue - - log.warning("Unknown syntax of aperture macro part: %s" % str(part)) - - def append(self, data): - """ - Appends a string to the raw macro. - - :param data: Part of the macro. - :type data: str - :return: None - """ - self.raw += data - - @staticmethod - def default2zero(n, mods): - """ - Pads the ``mods`` list with zeros resulting in an - list of length n. - - :param n: Length of the resulting list. - :type n: int - :param mods: List to be padded. - :type mods: list - :return: Zero-padded list. - :rtype: list - """ - x = [0.0] * n - na = len(mods) - x[0:na] = mods - return x - - @staticmethod - def make_circle(mods): - """ - - :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord) - :return: - """ - - pol, dia, x, y = ApertureMacro.default2zero(4, mods) - - return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)} - - @staticmethod - def make_vectorline(mods): - """ - - :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end, - rotation angle around origin in degrees) - :return: - """ - pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods) - - line = LineString([(xs, ys), (xe, ye)]) - box = line.buffer(width/2, cap_style=2) - box_rotated = affinity.rotate(box, angle, origin=(0, 0)) - - return {"pol": int(pol), "geometry": box_rotated} - - @staticmethod - def make_centerline(mods): - """ - - :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center, - rotation angle around origin in degrees) - :return: - """ - - pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods) - - box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2) - box_rotated = affinity.rotate(box, angle, origin=(0, 0)) - - return {"pol": int(pol), "geometry": box_rotated} - - @staticmethod - def make_lowerleftline(mods): - """ - - :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft, - rotation angle around origin in degrees) - :return: - """ - - pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods) - - box = shply_box(x, y, x+width, y+height) - box_rotated = affinity.rotate(box, angle, origin=(0, 0)) - - return {"pol": int(pol), "geometry": box_rotated} - - @staticmethod - def make_outline(mods): - """ - - :param mods: - :return: - """ - - pol = mods[0] - n = mods[1] - points = [(0, 0)]*(n+1) - - for i in range(n+1): - points[i] = mods[2*i + 2:2*i + 4] - - angle = mods[2*n + 4] - - poly = Polygon(points) - poly_rotated = affinity.rotate(poly, angle, origin=(0, 0)) - - return {"pol": int(pol), "geometry": poly_rotated} - - @staticmethod - def make_polygon(mods): - """ - Note: Specs indicate that rotation is only allowed if the center - (x, y) == (0, 0). I will tolerate breaking this rule. - - :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center, - diameter of circumscribed circle >=0, rotation angle around origin) - :return: - """ - - pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods) - points = [(0, 0)]*nverts - - for i in range(nverts): - points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts), - y + 0.5 * dia * sin(2*pi * i/nverts)) - - poly = Polygon(points) - poly_rotated = affinity.rotate(poly, angle, origin=(0, 0)) - - return {"pol": int(pol), "geometry": poly_rotated} - - @staticmethod - def make_moire(mods): - """ - Note: Specs indicate that rotation is only allowed if the center - (x, y) == (0, 0). I will tolerate breaking this rule. - - :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness, - gap, max_rings, crosshair_thickness, crosshair_len, rotation - angle around origin in degrees) - :return: - """ - - x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods) - - r = dia/2 - thickness/2 - result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) - ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) # Need a copy! - - i = 1 # Number of rings created so far - - ## If the ring does not have an interior it means that it is - ## a disk. Then stop. - while len(ring.interiors) > 0 and i < nrings: - r -= thickness + gap - if r <= 0: - break - ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) - result = cascaded_union([result, ring]) - i += 1 - - ## Crosshair - hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2) - ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2) - result = cascaded_union([result, hor, ver]) - - return {"pol": 1, "geometry": result} - - @staticmethod - def make_thermal(mods): - """ - Note: Specs indicate that rotation is only allowed if the center - (x, y) == (0, 0). I will tolerate breaking this rule. - - :param mods: [x-center, y-center, diameter-outside, diameter-inside, - gap-thickness, rotation angle around origin] - :return: - """ - - x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods) - - ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0)) - hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3) - vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3) - thermal = ring.difference(hline.union(vline)) - - return {"pol": 1, "geometry": thermal} - - def make_geometry(self, modifiers): - """ - Runs the macro for the given modifiers and generates - the corresponding geometry. - - :param modifiers: Modifiers (parameters) for this macro - :type modifiers: list - :return: Shapely geometry - :rtype: shapely.geometry.polygon - """ - - ## Primitive makers - makers = { - "1": ApertureMacro.make_circle, - "2": ApertureMacro.make_vectorline, - "20": ApertureMacro.make_vectorline, - "21": ApertureMacro.make_centerline, - "22": ApertureMacro.make_lowerleftline, - "4": ApertureMacro.make_outline, - "5": ApertureMacro.make_polygon, - "6": ApertureMacro.make_moire, - "7": ApertureMacro.make_thermal - } - - ## Store modifiers as local variables - modifiers = modifiers or [] - modifiers = [float(m) for m in modifiers] - self.locvars = {} - for i in range(0, len(modifiers)): - self.locvars[str(i + 1)] = modifiers[i] - - ## Parse - self.primitives = [] # Cleanup - self.geometry = Polygon() - self.parse_content() - - ## Make the geometry - for primitive in self.primitives: - # Make the primitive - 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 prim_geo['pol'] == 1: - self.geometry = self.geometry.union(prim_geo['geometry']) - continue - if prim_geo['pol'] == 0: - self.geometry = self.geometry.difference(prim_geo['geometry']) - continue - - return self.geometry - - -class Gerber (Geometry): - """ - **ATTRIBUTES** - - * ``apertures`` (dict): The keys are names/identifiers of each aperture. - The values are dictionaries key/value pairs which describe the aperture. The - type key is always present and the rest depend on the key: - - +-----------+-----------------------------------+ - | Key | Value | - +===========+===================================+ - | type | (str) "C", "R", "O", "P", or "AP" | - +-----------+-----------------------------------+ - | others | Depend on ``type`` | - +-----------+-----------------------------------+ - - * ``aperture_macros`` (dictionary): Are predefined geometrical structures - that can be instanciated with different parameters in an aperture - definition. See ``apertures`` above. The key is the name of the macro, - and the macro itself, the value, is a ``Aperture_Macro`` object. - - * ``flash_geometry`` (list): List of (Shapely) geometric object resulting - from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``. - - * ``buffered_paths`` (list): List of (Shapely) polygons resulting from - *buffering* (or thickening) the ``paths`` with the aperture. These are - generated from ``paths`` in ``buffer_paths()``. - - **USAGE**:: - - g = Gerber() - g.parse_file(filename) - g.create_geometry() - do_something(s.solid_geometry) - - """ - - defaults = { - "steps_per_circle": 40, - "use_buffer_for_union": True - } - - def __init__(self, steps_per_circle=None): - """ - The constructor takes no parameters. Use ``gerber.parse_files()`` - or ``gerber.parse_lines()`` to populate the object from Gerber source. - - :return: Gerber object - :rtype: Gerber - """ - - # Initialize parent - Geometry.__init__(self) - - self.solid_geometry = Polygon() - - # Number format - self.int_digits = 3 - """Number of integer digits in Gerber numbers. Used during parsing.""" - - self.frac_digits = 4 - """Number of fraction digits in Gerber numbers. Used during parsing.""" - - ## Gerber elements ## - # Apertures {'id':{'type':chr, - # ['size':float], ['width':float], - # ['height':float]}, ...} - self.apertures = {} - - # Aperture Macros - self.aperture_macros = {} - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from Geometry. - self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', - 'aperture_macros', 'solid_geometry'] - - #### Parser patterns #### - # FS - Format Specification - # The format of X and Y must be the same! - # L-omit leading zeros, T-omit trailing zeros - # A-absolute notation, I-incremental notation - self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$') - - # Mode (IN/MM) - self.mode_re = re.compile(r'^%MO(IN|MM)\*%$') - - # Comment G04|G4 - self.comm_re = re.compile(r'^G0?4(.*)$') - - # AD - Aperture definition - # 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 *%): - #self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*') - - # Tool change - # May begin with G54 but that is deprecated - self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$') - - # G01... - Linear interpolation plus flashes with coordinates - # Operation code (D0x) missing is deprecated... oh well I will support it. - self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$') - - # Operation code alone, usually just D03 (Flash) - self.opcode_re = re.compile(r'^D0?([123])\*$') - - # G02/3... - Circular interpolation with coordinates - # 2-clockwise, 3-counterclockwise - # Operation code (D0x) missing is deprecated... oh well I will support it. - # Optional start with G02 or G03, optional end with D01 or D02 with - # optional coordinates but at least one in any order. - self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' + - '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$') - - # G01/2/3 Occurring without coordinates - self.interp_re = re.compile(r'^(?:G0?([123]))\*') - - # Single D74 or multi D75 quadrant for circular interpolation - self.quad_re = re.compile(r'^G7([45])\*$') - - # Region mode on - # In region mode, D01 starts a region - # and D02 ends it. A new region can be started again - # with D01. All contours must be closed before - # D02 or G37. - self.regionon_re = re.compile(r'^G36\*$') - - # Region mode off - # Will end a region and come off region mode. - # All contours must be closed before D02 or G37. - self.regionoff_re = re.compile(r'^G37\*$') - - # End of file - self.eof_re = re.compile(r'^M02\*') - - # IP - Image polarity - self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$') - - # LP - Level polarity - self.lpol_re = re.compile(r'^%LP([DC])\*%$') - - # Units (OBSOLETE) - self.units_re = re.compile(r'^G7([01])\*$') - - # Absolute/Relative G90/1 (OBSOLETE) - self.absrel_re = re.compile(r'^G9([01])\*$') - - # Aperture macros - self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$') - self.am2_re = re.compile(r'(.*)%$') - - # How to discretize a circle. - self.steps_per_circ = steps_per_circle or Gerber.defaults['steps_per_circle'] - - self.use_buffer_for_union = self.defaults["use_buffer_for_union"] - - def scale(self, factor): - """ - Scales the objects' geometry on the XY plane by a given factor. - These are: - - * ``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 factor: Number by which to scale. - :type factor: float - :rtype : None - """ - - ## solid_geometry ??? - # It's a cascaded union of objects. - self.solid_geometry = affinity.scale(self.solid_geometry, factor, - factor, origin=(0, 0)) - - # # Now buffered_paths, flash_geometry and solid_geometry - # self.create_geometry() - - def offset(self, vect): - """ - Offsets the objects' geometry on the XY plane by a given vector. - These are: - - * ``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 vect: (x, y) offset vector. - :type vect: tuple - :return: None - """ - - dx, dy = vect - - ## 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 aperture_parse(self, apertureId, apertureType, apParameters): - """ - Parse gerber aperture definition into dictionary of apertures. - The following kinds and their attributes are supported: - - * *Circular (C)*: size (float) - * *Rectangle (R)*: width (float), height (float) - * *Obround (O)*: width (float), height (float). - * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)] - * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list) - - :param apertureId: Id of the aperture being defined. - :param apertureType: Type of the aperture. - :param apParameters: Parameters of the aperture. - :type apertureId: str - :type apertureType: str - :type apParameters: str - :return: Identifier of the aperture. - :rtype: str - """ - - # Found some Gerber with a leading zero in the aperture id and the - # referenced it without the zero, so this is a hack to handle that. - apid = str(int(apertureId)) - - try: # Could be empty for aperture macros - paramList = apParameters.split('X') - except: - paramList = None - - if apertureType == "C": # Circle, example: %ADD11C,0.1*% - self.apertures[apid] = {"type": "C", - "size": float(paramList[0])} - return apid - - if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*% - self.apertures[apid] = {"type": "R", - "width": float(paramList[0]), - "height": float(paramList[1]), - "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack - return apid - - if apertureType == "O": # Obround - self.apertures[apid] = {"type": "O", - "width": float(paramList[0]), - "height": float(paramList[1]), - "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack - return apid - - if apertureType == "P": # Polygon (regular) - self.apertures[apid] = {"type": "P", - "diam": float(paramList[0]), - "nVertices": int(paramList[1]), - "size": float(paramList[0])} # Hack - if len(paramList) >= 3: - self.apertures[apid]["rotation"] = float(paramList[2]) - return apid - - if apertureType in self.aperture_macros: - self.apertures[apid] = {"type": "AM", - "macro": self.aperture_macros[apertureType], - "modifiers": paramList} - return apid - - log.warning("Aperture not implemented: %s" % str(apertureType)) - return None - - def parse_file(self, filename, follow=False): - """ - Calls Gerber.parse_lines() with generator of lines - read from the given file. Will split the lines if multiple - statements are found in a single original line. - - The following line is split into two:: - - G54D11*G36* - - First is ``G54D11*`` and seconds is ``G36*``. - - :param filename: Gerber file to parse. - :type filename: str - :param follow: If true, will not create polygons, just lines - following the gerber path. - :type follow: bool - :return: None - """ - - with open(filename, 'r') as gfile: - - def line_generator(): - for line in gfile: - line = line.strip(' \r\n') - while len(line) > 0: - - # If ends with '%' leave as is. - if line[-1] == '%': - yield line - break - - # Split after '*' if any. - starpos = line.find('*') - if starpos > -1: - cleanline = line[:starpos + 1] - yield cleanline - line = line[starpos + 1:] - - # Otherwise leave as is. - else: - # yield cleanline - yield line - break - - self.parse_lines(line_generator(), follow=follow) - - #@profile - def parse_lines(self, glines, follow=False): - """ - Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, - ``self.flashes``, ``self.regions`` and ``self.units``. - - :param glines: Gerber code as list of strings, each element being - one line of the source file. - :type glines: list - :param follow: If true, will not create polygons, just lines - following the gerber path. - :type follow: bool - :return: None - :rtype: None - """ - - # Coordinates of the current path, each is [x, y] - path = [] - - # Polygons are stored here until there is a change in polarity. - # Only then they are combined via cascaded_union and added or - # subtracted from solid_geometry. This is ~100 times faster than - # applyng a union for every new polygon. - poly_buffer = [] - - last_path_aperture = None - current_aperture = None - - # 1,2 or 3 from "G01", "G02" or "G03" - current_interpolation_mode = None - - # 1 or 2 from "D01" or "D02" - # Note this is to support deprecated Gerber not putting - # an operation code at the end of every coordinate line. - current_operation_code = None - - # Current coordinates - current_x = None - current_y = None - - # Absolute or Relative/Incremental coordinates - # Not implemented - absolute = True - - # How to interpret circular interpolation: SINGLE or MULTI - quadrant_mode = None - - # Indicates we are parsing an aperture macro - current_macro = None - - # Indicates the current polarity: D-Dark, C-Clear - current_polarity = 'D' - - # If a region is being defined - making_region = False - - #### Parsing starts here #### - line_num = 0 - gline = "" - try: - for gline in glines: - line_num += 1 - - ### Cleanup - gline = gline.strip(' \r\n') - - #log.debug("%3s %s" % (line_num, gline)) - - ### Aperture Macros - # Having this at the beginning will slow things down - # but macros can have complicated statements than could - # be caught by other patterns. - if current_macro is None: # No macro started yet - match = self.am1_re.search(gline) - # Start macro if match, else not an AM, carry on. - if match: - 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 - self.aperture_macros[current_macro].append(match.group(2)) - if match.group(3): # Finish macro - #self.aperture_macros[current_macro].parse_content() - current_macro = None - log.debug("Macro complete in 1 line.") - continue - else: # Continue macro - log.debug("Continuing macro. Line %d." % line_num) - match = self.am2_re.search(gline) - if match: # Finish macro - 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 - else: # Append - self.aperture_macros[current_macro].append(gline) - continue - - ### G01 - Linear interpolation plus flashes - # Operation code (D0x) missing is deprecated... oh well I will support it. - # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$' - match = self.lin_re.search(gline) - if match: - # Dxx alone? - # if match.group(1) is None and match.group(2) is None and match.group(3) is None: - # try: - # current_operation_code = int(match.group(4)) - # except: - # pass # A line with just * will match too. - # continue - # NOTE: Letting it continue allows it to react to the - # operation code. - - # Parse coordinates - if match.group(2) is not None: - current_x = parse_gerber_number(match.group(2), self.frac_digits) - if match.group(3) is not None: - current_y = parse_gerber_number(match.group(3), self.frac_digits) - - # Parse operation code - if match.group(4) is not None: - current_operation_code = int(match.group(4)) - - # Pen down: add segment - if current_operation_code == 1: - path.append([current_x, current_y]) - last_path_aperture = current_aperture - - elif current_operation_code == 2: - if len(path) > 1: - - ## --- BUFFERED --- - if making_region: - 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) - width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail! - #log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture)) - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 2) - - if not geo.is_empty: - poly_buffer.append(geo) - - path = [[current_x, current_y]] # Start new path - - # Flash - # Not allowed in region mode. - elif current_operation_code == 3: - - # Create path draw so far. - if len(path) > 1: - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - - 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) - - continue - - ### G02/3 - Circular interpolation - # 2-clockwise, 3-counterclockwise - match = self.circ_re.search(gline) - if match: - arcdir = [None, None, "cw", "ccw"] - - mode, x, y, i, j, d = match.groups() - try: - x = parse_gerber_number(x, self.frac_digits) - except: - x = current_x - try: - y = parse_gerber_number(y, self.frac_digits) - except: - y = current_y - try: - i = parse_gerber_number(i, self.frac_digits) - except: - i = 0 - try: - j = parse_gerber_number(j, self.frac_digits) - except: - j = 0 - - if quadrant_mode is None: - log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num) - log.error(gline) - continue - - if mode is None and current_interpolation_mode not in [2, 3]: - log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) - log.error(gline) - continue - elif mode is not None: - current_interpolation_mode = int(mode) - - # Set operation code if provided - if d is not None: - current_operation_code = int(d) - - # Nothing created! Pen Up. - if current_operation_code == 2: - log.warning("Arc with D2. (%d)" % line_num) - if len(path) > 1: - if last_path_aperture is None: - log.warning("No aperture defined for curent path. (%d)" % line_num) - - # --- BUFFERED --- - width = self.apertures[last_path_aperture]["size"] - - 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 - path = [[current_x, current_y]] # Start new path - continue - - # Flash should not happen here - if current_operation_code == 3: - log.error("Trying to flash within arc. (%d)" % line_num) - continue - - if quadrant_mode == 'MULTI': - center = [i + current_x, j + current_y] - radius = sqrt(i ** 2 + j ** 2) - start = arctan2(-j, -i) # Start angle - # Numerical errors might prevent start == stop therefore - # we check ahead of time. This should result in a - # 360 degree arc. - if current_x == x and current_y == y: - stop = start - else: - stop = arctan2(-center[1] + y, -center[0] + x) # Stop angle - - this_arc = arc(center, radius, start, stop, - arcdir[current_interpolation_mode], - self.steps_per_circ) - - # The last point in the computed arc can have - # numerical errors. The exact final point is the - # specified (x, y). Replace. - this_arc[-1] = (x, y) - - # Last point in path is current point - # current_x = this_arc[-1][0] - # current_y = this_arc[-1][1] - current_x, current_y = x, y - - # Append - path += this_arc - - last_path_aperture = current_aperture - - continue - - if quadrant_mode == 'SINGLE': - - center_candidates = [ - [i + current_x, j + current_y], - [-i + current_x, j + current_y], - [i + current_x, -j + current_y], - [-i + current_x, -j + current_y] - ] - - valid = False - log.debug("I: %f J: %f" % (i, j)) - for center in center_candidates: - radius = sqrt(i ** 2 + j ** 2) - - # Make sure radius to start is the same as radius to end. - radius2 = sqrt((center[0] - x) ** 2 + (center[1] - y) ** 2) - if radius2 < radius * 0.95 or radius2 > radius * 1.05: - continue # Not a valid center. - - # Correct i and j and continue as with multi-quadrant. - i = center[0] - current_x - j = center[1] - current_y - - start = arctan2(-j, -i) # Start angle - stop = arctan2(-center[1] + y, -center[0] + x) # Stop angle - angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode])) - log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" % - (current_x, current_y, center[0], center[1], x, y)) - log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" % - (start * 180 / pi, stop * 180 / pi, arcdir[current_interpolation_mode], - angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2)) - - if angle <= (pi + 1e-6) / 2: - log.debug("########## ACCEPTING ARC ############") - this_arc = arc(center, radius, start, stop, - arcdir[current_interpolation_mode], - self.steps_per_circ) - - # Replace with exact values - this_arc[-1] = (x, y) - - # current_x = this_arc[-1][0] - # current_y = this_arc[-1][1] - current_x, current_y = x, y - - path += this_arc - last_path_aperture = current_aperture - valid = True - break - - if valid: - continue - else: - log.warning("Invalid arc in line %d." % line_num) - - ### Operation code alone - # Operation code alone, usually just D03 (Flash) - # self.opcode_re = re.compile(r'^D0?([123])\*$') - match = self.opcode_re.search(gline) - if match: - current_operation_code = int(match.group(1)) - if current_operation_code == 3: - - ## --- Buffered --- - try: - 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) - except IndexError: - log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline)) - - continue - - ### G74/75* - Single or multiple quadrant arcs - match = self.quad_re.search(gline) - if match: - if match.group(1) == '4': - quadrant_mode = 'SINGLE' - else: - quadrant_mode = 'MULTI' - continue - - ### G36* - Begin region - if self.regionon_re.search(gline): - if len(path) > 1: - # Take care of what is left in the path - - ## --- Buffered --- - width = self.apertures[last_path_aperture]["size"] - - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width/2) - if not geo.is_empty: - poly_buffer.append(geo) - - path = [path[-1]] - - making_region = True - continue - - ### G37* - End region - if self.regionoff_re.search(gline): - making_region = False - - # Only one path defines region? - # This can happen if D02 happened before G37 and - # is not and error. - if len(path) < 3: - # print "ERROR: Path contains less than 3 points:" - # print path - # print "Line (%d): " % line_num, gline - # path = [] - #path = [[current_x, current_y]] - continue - - # For regions we may ignore an aperture that is None - # self.regions.append({"polygon": Polygon(path), - # "aperture": last_path_aperture}) - - # --- Buffered --- - if follow: - region = Polygon() - else: - region = Polygon(path) - if not region.is_valid: - 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 - - ### Aperture definitions %ADD... - match = self.ad_re.search(gline) - if match: - log.info("Found aperture definition. Line %d: %s" % (line_num, gline)) - self.aperture_parse(match.group(1), match.group(2), match.group(3)) - continue - - ### G01/2/3* - Interpolation mode change - # Can occur along with coordinates and operation code but - # sometimes by itself (handled here). - # Example: G01* - match = self.interp_re.search(gline) - if match: - current_interpolation_mode = int(match.group(1)) - continue - - ### Tool/aperture change - # Example: D12* - match = self.tool_re.search(gline) - if match: - current_aperture = match.group(1) - log.debug("Line %d: Aperture change to (%s)" % (line_num, match.group(1))) - log.debug(self.apertures[current_aperture]) - - # If the aperture value is zero then make it something quite small but with a non-zero value - # so it can be processed by FlatCAM. - # But first test to see if the aperture type is "aperture macro". In that case - # we should not test for "size" key as it does not exist in this case. - if self.apertures[current_aperture]["type"] is not "AM": - if self.apertures[current_aperture]["size"] == 0: - self.apertures[current_aperture]["size"] = 0.0000001 - log.debug(self.apertures[current_aperture]) - - # Take care of the current path with the previous tool - if len(path) > 1: - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - - 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 - - ### Polarity change - # Example: %LPD*% or %LPC*% - # If polarity changes, creates geometry from current - # buffer, then adds or subtracts accordingly. - match = self.lpol_re.search(gline) - if match: - if len(path) > 1 and current_polarity != match.group(1): - - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 2) - if not geo.is_empty: - poly_buffer.append(geo) - - path = [path[-1]] - - # --- Apply buffer --- - # If added for testing of bug #83 - # TODO: Remove when bug fixed - if len(poly_buffer) > 0: - if current_polarity == 'D': - self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) - else: - self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) - poly_buffer = [] - - current_polarity = match.group(1) - continue - - ### Number format - # Example: %FSLAX24Y24*% - # TODO: This is ignoring most of the format. Implement the rest. - match = self.fmt_re.search(gline) - if match: - absolute = {'A': True, 'I': False} - self.int_digits = int(match.group(3)) - self.frac_digits = int(match.group(4)) - continue - - ### Mode (IN/MM) - # Example: %MOIN*% - match = self.mode_re.search(gline) - if match: - #self.units = match.group(1) - - # Changed for issue #80 - self.convert_units(match.group(1)) - continue - - ### Units (G70/1) OBSOLETE - match = self.units_re.search(gline) - if match: - #self.units = {'0': 'IN', '1': 'MM'}[match.group(1)] - - # Changed for issue #80 - self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)]) - continue - - ### Absolute/relative coordinates G90/1 OBSOLETE - match = self.absrel_re.search(gline) - if match: - absolute = {'0': True, '1': False}[match.group(1)] - continue - - #### Ignored lines - ## Comments - match = self.comm_re.search(gline) - if match: - continue - - ## EOF - match = self.eof_re.search(gline) - if match: - continue - - ### Line did not match any pattern. Warn user. - log.warning("Line ignored (%d): %s" % (line_num, gline)) - - if len(path) > 1: - # EOF, create shapely LineString if something still in path - - ## --- Buffered --- - width = self.apertures[last_path_aperture]["size"] - 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...") - new_poly = MultiPolygon(poly_buffer) - new_poly = new_poly.buffer(0.00000001) - new_poly = new_poly.buffer(-0.00000001) - log.warn("Union(buffer) done.") - else: - log.debug("Union by union()...") - new_poly = cascaded_union(poly_buffer) - new_poly = new_poly.buffer(0) - log.warn("Union done.") - if current_polarity == 'D': - self.solid_geometry = self.solid_geometry.union(new_poly) - else: - self.solid_geometry = self.solid_geometry.difference(new_poly) - - except Exception as err: - ex_type, ex, tb = sys.exc_info() - traceback.print_tb(tb) - #print traceback.format_exc() - - log.error("PARSING FAILED. Line %d: %s" % (line_num, gline)) - raise ParseError("Line %d: %s" % (line_num, gline), repr(err)) - - @staticmethod - def create_flash_geometry(location, aperture): - - log.debug('Flashing @%s, Aperture: %s' % (location, aperture)) - - if type(location) == list: - location = Point(location) - - if aperture['type'] == 'C': # Circles - return location.buffer(aperture['size'] / 2) - - if aperture['type'] == 'R': # Rectangles - loc = location.coords[0] - width = aperture['width'] - height = aperture['height'] - minx = loc[0] - width / 2 - maxx = loc[0] + width / 2 - miny = loc[1] - height / 2 - maxy = loc[1] + height / 2 - return shply_box(minx, miny, maxx, maxy) - - if aperture['type'] == 'O': # Obround - loc = location.coords[0] - width = aperture['width'] - height = aperture['height'] - if width > height: - p1 = Point(loc[0] + 0.5 * (width - height), loc[1]) - p2 = Point(loc[0] - 0.5 * (width - height), loc[1]) - c1 = p1.buffer(height * 0.5) - c2 = p2.buffer(height * 0.5) - else: - p1 = Point(loc[0], loc[1] + 0.5 * (height - width)) - p2 = Point(loc[0], loc[1] - 0.5 * (height - width)) - c1 = p1.buffer(width * 0.5) - c2 = p2.buffer(width * 0.5) - return cascaded_union([c1, c2]).convex_hull - - if aperture['type'] == 'P': # Regular polygon - loc = location.coords[0] - diam = aperture['diam'] - n_vertices = aperture['nVertices'] - points = [] - for i in range(0, n_vertices): - x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices)) - y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices)) - points.append((x, y)) - ply = Polygon(points) - if 'rotation' in aperture: - ply = affinity.rotate(ply, aperture['rotation']) - return ply - - 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): - """ - Geometry from a Gerber file is made up entirely of polygons. - Every stroke (linear or circular) has an aperture which gives - it thickness. Additionally, aperture strokes have non-zero area, - and regions naturally do as well. - - :rtype : None - :return: None - """ - - # self.buffer_paths() - # - # self.fix_regions() - # - # self.do_flashes() - # - # self.solid_geometry = cascaded_union(self.buffered_paths + - # [poly['polygon'] for poly in self.regions] + - # self.flash_geometry) - - def get_bounding_box(self, margin=0.0, rounded=False): - """ - Creates and returns a rectangular polygon bounding at a distance of - margin from the object's ``solid_geometry``. If margin > 0, the polygon - can optionally have rounded corners of radius equal to margin. - - :param margin: Distance to enlarge the rectangular bounding - box in both positive and negative, x and y axes. - :type margin: float - :param rounded: Wether or not to have rounded corners. - :type rounded: bool - :return: The bounding box. - :rtype: Shapely.Polygon - """ - - bbox = self.solid_geometry.envelope.buffer(margin) - if not rounded: - bbox = bbox.envelope - return bbox - - -class Excellon(Geometry): - """ - *ATTRIBUTES* - - * ``tools`` (dict): The key is the tool name and the value is - a dictionary specifying the tool: - - ================ ==================================== - Key Value - ================ ==================================== - C Diameter of the tool - Others Not supported (Ignored). - ================ ==================================== - - * ``drills`` (list): Each is a dictionary: - - ================ ==================================== - Key Value - ================ ==================================== - point (Shapely.Point) Where to drill - tool (str) A key in ``tools`` - ================ ==================================== - """ - - defaults = { - "zeros": "L" - } - - def __init__(self, zeros=None): - """ - The constructor takes no parameters. - - :return: Excellon object. - :rtype: Excellon - """ - - Geometry.__init__(self) - - # self.tools[name] = {"C": diameter} - self.tools = {} - self.drills = [] - - ## IN|MM -> Units are inherited from Geometry - #self.units = units - - # Trailing "T" or leading "L" (default) - #self.zeros = "T" - self.zeros = zeros or self.defaults["zeros"] - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from Geometry. - self.ser_attrs += ['tools', 'drills', 'zeros'] - - #### Patterns #### - # Regex basics: - # ^ - beginning - # $ - end - # *: 0 or more, +: 1 or more, ?: 0 or 1 - - # M48 - Beggining of Part Program Header - self.hbegin_re = re.compile(r'^M48$') - - # M95 or % - End of Part Program Header - # NOTE: % has different meaning in the body - self.hend_re = re.compile(r'^(?:M95|%)$') - - # FMAT Excellon format - # Ignored in the parser - #self.fmat_re = re.compile(r'^FMAT,([12])$') - - # Number format and units - # INCH uses 6 digits - # METRIC uses 5/6 - self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$') - - # Tool definition/parameters (?= is look-ahead - # NOTE: This might be an overkill! - # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' + - # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + - # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + - # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') - self.toolset_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))?' + - r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + - r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + - r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') - - # Tool select - # Can have additional data after tool number but - # is ignored if present in the header. - # Warning: This will match toolset_re too. - # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))') - self.toolsel_re = re.compile(r'^T(\d+)') - - # Comment - self.comm_re = re.compile(r'^;(.*)$') - - # Absolute/Incremental G90/G91 - self.absinc_re = re.compile(r'^G9([01])$') - - # Modes of operation - # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill - self.modes_re = re.compile(r'^G0([012345])') - - # Measuring mode - # 1-metric, 2-inch - self.meas_re = re.compile(r'^M7([12])$') - - # Coordinates - #self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$') - #self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$') - self.coordsperiod_re = re.compile(r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]') - self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]') - - # R - Repeat hole (# times, X offset, Y offset) - self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$') - - # Various stop/pause commands - self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))') - - # Parse coordinates - self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)') - - def parse_file(self, filename): - """ - Reads the specified file as array of lines as - passes it to ``parse_lines()``. - - :param filename: The file to be read and parsed. - :type filename: str - :return: None - """ - efile = open(filename, 'r') - estr = efile.readlines() - efile.close() - self.parse_lines(estr) - - def parse_lines(self, elines): - """ - Main Excellon parser. - - :param elines: List of strings, each being a line of Excellon code. - :type elines: list - :return: None - """ - - # State variables - current_tool = "" - in_header = False - current_x = None - current_y = None - - #### Parsing starts here #### - line_num = 0 # Line number - eline = "" - try: - for eline in elines: - line_num += 1 - #log.debug("%3d %s" % (line_num, str(eline))) - - ### Cleanup lines - eline = eline.strip(' \r\n') - - ## Header Begin (M48) ## - if self.hbegin_re.search(eline): - in_header = True - continue - - ## Header End ## - if self.hend_re.search(eline): - in_header = False - continue - - ## Alternative units format M71/M72 - # Supposed to be just in the body (yes, the body) - # but some put it in the header (PADS for example). - # Will detect anywhere. Occurrence will change the - # object's units. - match = self.meas_re.match(eline) - if match: - #self.units = {"1": "MM", "2": "IN"}[match.group(1)] - - # Modified for issue #80 - self.convert_units({"1": "MM", "2": "IN"}[match.group(1)]) - log.debug(" Units: %s" % self.units) - continue - - #### Body #### - if not in_header: - - ## Tool change ## - match = self.toolsel_re.search(eline) - if match: - current_tool = str(int(match.group(1))) - log.debug("Tool change: %s" % current_tool) - continue - - ## Coordinates without period ## - match = self.coordsnoperiod_re.search(eline) - if match: - try: - #x = float(match.group(1))/10000 - x = self.parse_number(match.group(1)) - current_x = x - except TypeError: - x = current_x - - try: - #y = float(match.group(2))/10000 - y = self.parse_number(match.group(2)) - current_y = y - except TypeError: - y = current_y - - if x is None or y is None: - log.error("Missing coordinates") - continue - - self.drills.append({'point': Point((x, y)), 'tool': current_tool}) - log.debug("{:15} {:8} {:8}".format(eline, x, y)) - continue - - ## Coordinates with period: Use literally. ## - match = self.coordsperiod_re.search(eline) - if match: - try: - x = float(match.group(1)) - current_x = x - except TypeError: - x = current_x - - try: - y = float(match.group(2)) - current_y = y - except TypeError: - y = current_y - - if x is None or y is None: - log.error("Missing coordinates") - continue - - self.drills.append({'point': Point((x, y)), 'tool': current_tool}) - log.debug("{:15} {:8} {:8}".format(eline, x, y)) - continue - - #### Header #### - if in_header: - - ## Tool definitions ## - match = self.toolset_re.search(eline) - if match: - - name = str(int(match.group(1))) - spec = { - "C": float(match.group(2)), - # "F": float(match.group(3)), - # "S": float(match.group(4)), - # "B": float(match.group(5)), - # "H": float(match.group(6)), - # "Z": float(match.group(7)) - } - self.tools[name] = spec - log.debug(" Tool definition: %s %s" % (name, spec)) - continue - - ## Units and number format ## - match = self.units_re.match(eline) - if match: - self.zeros = match.group(2) or self.zeros # "T" or "L". Might be empty - - #self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)] - - # Modified for issue #80 - self.convert_units({"INCH": "IN", "METRIC": "MM"}[match.group(1)]) - log.debug(" Units/Format: %s %s" % (self.units, self.zeros)) - continue - - log.warning("Line ignored: %s" % eline) - - log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) - - except Exception as e: - log.error("PARSING FAILED. Line %d: %s" % (line_num, eline)) - raise - - def parse_number(self, number_str): - """ - Parses coordinate numbers without period. - - :param number_str: String representing the numerical value. - :type number_str: str - :return: Floating point representation of the number - :rtype: foat - """ - if self.zeros == "L": - # With leading zeros, when you type in a coordinate, - # the leading zeros must always be included. Trailing zeros - # are unneeded and may be left off. The CNC-7 will automatically add them. - # r'^[-\+]?(0*)(\d*)' - # 6 digits are divided by 10^4 - # If less than size digits, they are automatically added, - # 5 digits then are divided by 10^3 and so on. - match = self.leadingzeros_re.search(number_str) - if self.units.lower() == "in": - return float(number_str) / \ - (10 ** (len(match.group(1)) + len(match.group(2)) - 2)) - else: - return float(number_str) / \ - (10 ** (len(match.group(1)) + len(match.group(2)) - 3)) - - else: # Trailing - # You must show all zeros to the right of the number and can omit - # all zeros to the left of the number. The CNC-7 will count the number - # of digits you typed and automatically fill in the missing zeros. - if self.units.lower() == "in": # Inches is 00.0000 - return float(number_str) / 10000 - else: - return float(number_str) / 1000 # Metric is 000.000 - - def create_geometry(self): - """ - Creates circles of the tool diameter at every point - specified in ``self.drills``. - - :return: None - """ - self.solid_geometry = [] - - for drill in self.drills: - # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0) - tooldia = self.tools[drill['tool']]['C'] - poly = drill['point'].buffer(tooldia / 2.0) - self.solid_geometry.append(poly) - - def scale(self, factor): - """ - Scales geometry on the XY plane in the object by a given factor. - Tool sizes, feedrates an Z-plane dimensions are untouched. - - :param factor: Number by which to scale the object. - :type factor: float - :return: None - :rtype: NOne - """ - - # Drills - for drill in self.drills: - drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0)) - - self.create_geometry() - - def offset(self, vect): - """ - Offsets geometry on the XY plane in the object by a given vector. - - :param vect: (x, y) offset vector. - :type vect: tuple - :return: None - """ - - dx, dy = vect - - # Drills - for drill in self.drills: - drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy) - - # Recreate geometry - self.create_geometry() - - def mirror(self, axis, 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] - - # Modify data - for drill in self.drills: - drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py)) - - # Recreate geometry - self.create_geometry() - - def skew(self, angle_x=None, angle_y=None, point=None): - """ - Shear/Skew the geometries of an object by angles along x and y dimensions. - Tool sizes, feedrates an Z-plane dimensions are untouched. - - Parameters - ---------- - angle_x, angle_y: float, float - The shear angle(s) for the x and y axes respectively. These can be - specified in either degrees (default) or radians by setting - use_radians=True. - point: point of origin for skew, tuple of coordinates - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - - if angle_y is None: - angle_y = 0.0 - if angle_x is None: - angle_x = 0.0 - if point is None: - # Drills - for drill in self.drills: - drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, - origin=(0, 0)) - else: - # Drills - px, py = point - for drill in self.drills: - drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, - origin=(px, py)) - - self.create_geometry() - - def rotate(self, angle, point=None): - """ - Rotate the geometry of an object by an angle around the 'point' coordinates - :param angle: - :param point: point around which to rotate - :return: - """ - if point is None: - # Drills - for drill in self.drills: - drill['point'] = affinity.rotate(drill['point'], angle, origin='center') - else: - # Drills - px, py = point - for drill in self.drills: - drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py)) - - self.create_geometry() - - def convert_units(self, units): - factor = Geometry.convert_units(self, units) - - # Tools - for tname in self.tools: - self.tools[tname]["C"] *= factor - - self.create_geometry() - - return factor - - -class CNCjob(Geometry): - """ - Represents work to be done by a CNC machine. - - *ATTRIBUTES* - - * ``gcode_parsed`` (list): Each is a dictionary: - - ===================== ========================================= - Key Value - ===================== ========================================= - geom (Shapely.LineString) Tool path (XY plane) - kind (string) "AB", A is "T" (travel) or - "C" (cut). B is "F" (fast) or "S" (slow). - ===================== ========================================= - """ - - defaults = { - "zdownrate": None, - "coordinate_format": "X%.4fY%.4f" - } - - def __init__(self, - units="in", - kind="generic", - z_move=0.1, - feedrate=3.0, - z_cut=-0.002, - tooldia=0.0, - zdownrate=None, - spindlespeed=None): - - Geometry.__init__(self) - self.kind = kind - self.units = units - self.z_cut = z_cut - self.z_move = z_move - self.feedrate = feedrate - self.tooldia = tooldia - self.unitcode = {"IN": "G20", "MM": "G21"} - # 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 = "" - self.input_geometry_bounds = None - self.gcode_parsed = None - self.steps_per_circ = 20 # Used when parsing G-code arcs - - if zdownrate is not None: - self.zdownrate = float(zdownrate) - elif CNCjob.defaults["zdownrate"] is not None: - self.zdownrate = float(CNCjob.defaults["zdownrate"]) - else: - self.zdownrate = None - - self.spindlespeed = spindlespeed - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from Geometry. - self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia', - 'gcode', 'input_geometry_bounds', 'gcode_parsed', - 'steps_per_circ'] - - def convert_units(self, units): - factor = Geometry.convert_units(self, units) - log.debug("CNCjob.convert_units()") - - self.z_cut *= factor - self.z_move *= factor - self.feedrate *= factor - self.tooldia *= factor - - return factor - - def generate_from_excellon_by_tool(self, exobj, tools="all", - toolchange=False, toolchangez=0.1): - """ - Creates gcode for this object from an Excellon object - for the specified tools. - - :param exobj: Excellon object to process - :type exobj: Excellon - :param tools: Comma separated tool names - :type: tools: str - :param toolchange: Use tool change sequence between tools. - :type toolchange: bool - :param toolchangez: Height at which to perform the tool change. - :type toolchangez: float - :return: None - :rtype: None - """ - - log.debug("Creating CNC Job from Excellon...") - - # Tools - - # Sort tools by diameter. items() -> [('name', diameter), ...] - #sorted_tools = sorted(list(exobj.tools.items()), key=lambda tl: tl[1]) - sort = [] - for k, v in exobj.tools.items(): - sort.append((k, v.get('C'))) - sorted_tools = sorted(sort, key=lambda t1: t1[1]) - - if tools == "all": - tools = [i[0] for i in sorted_tools] # List if ordered tool names. - log.debug("Tools 'all' and sorted are: %s" % str(tools)) - else: - selected_tools = [x.strip() for x in tools.split(",")] - selected_tools = [tl for tl in selected_tools if tl in 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 = {} - for drill in exobj.drills: - if drill['tool'] in tools: - try: - points[drill['tool']].append(drill['point']) - except KeyError: - points[drill['tool']] = [drill['point']] - - # log.debug("Found %d drills." % len(points)) - self.gcode = [] - - # Basic G-Code macros - t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n" - down = "G01 Z%.4f\n" % self.z_cut - up = "G00 Z%.4f\n" % self.z_move - up_to_zero = "G01 Z0\n" - - # Initialization - gcode = self.unitcode[self.units.upper()] + "\n" - gcode += self.absolutecode + "\n" - gcode += self.feedminutecode + "\n" - gcode += "F%.2f\n" % self.feedrate - gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height - - 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 - - # gcode += self.pausecode + "\n" - - for tool in tools: - - # 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_to_zero + up - - gcode += t % (0, 0) - gcode += "M05\n" # Spindle stop - - self.gcode = gcode - - def generate_from_geometry_2(self, - geometry, - append=True, - tooldia=None, - tolerance=0, - multidepth=False, - depthpercut=None): - """ - Second algorithm to generate from Geometry. - - ALgorithm description: - ---------------------- - Uses RTree to find the nearest path to follow. - - :param geometry: - :param append: - :param tooldia: - :param tolerance: - :param multidepth: If True, use multiple passes to reach - the desired depth. - :param depthpercut: Maximum depth in each pass. - :return: None - """ - assert isinstance(geometry, Geometry), \ - "Expected a Geometry, got %s" % type(geometry) - - log.debug("generate_from_geometry_2()") - - ## Flatten the geometry - # Only linear elements (no polygons) remain. - flat_geometry = geometry.flatten(pathonly=True) - log.debug("%d paths" % len(flat_geometry)) - - ## Index first and last points in paths - # What points to index. - def get_pts(o): - return [o.coords[0], o.coords[-1]] - - # Create the indexed storage. - storage = FlatCAMRTreeStorage() - storage.get_points = get_pts - - # Store the geometry - log.debug("Indexing geometry before generating G-Code...") - for shape in flat_geometry: - if shape is not None: # TODO: This shouldn't have happened. - storage.insert(shape) - - if tooldia is not None: - self.tooldia = tooldia - - # self.input_geometry_bounds = geometry.bounds() - - if not append: - self.gcode = "" - - # Initial G-Code - self.gcode = self.unitcode[self.units.upper()] + "\n" - self.gcode += self.absolutecode + "\n" - self.gcode += self.feedminutecode + "\n" - self.gcode += "F%.2f\n" % self.feedrate - self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height - if self.spindlespeed is not None: - 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" - - ## Iterate over geometry paths getting the nearest each time. - log.debug("Starting G-Code...") - path_count = 0 - current_pt = (0, 0) - pt, geo = storage.nearest(current_pt) - try: - while True: - path_count += 1 - #print "Current: ", "(%.3f, %.3f)" % current_pt - - # Remove before modifying, otherwise - # deletion will fail. - storage.remove(geo) - - # If last point in geometry is the nearest - # but prefer the first one if last point == first point - # then reverse coordinates. - if pt != geo.coords[0] and pt == geo.coords[-1]: - geo.coords = list(geo.coords)[::-1] - - #---------- Single depth/pass -------- - if not multidepth: - # G-code - # Note: self.linear2gcode() and self.point2gcode() will - # lower and raise the tool every time. - if type(geo) == LineString or type(geo) == LinearRing: - self.gcode += self.linear2gcode(geo, tolerance=tolerance) - elif type(geo) == Point: - self.gcode += self.point2gcode(geo) - else: - log.warning("G-code generation not implemented for %s" % (str(type(geo)))) - - #--------- 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 = z_cut - elif not isinstance(depthpercut, Decimal): - depthpercut = Decimal(depthpercut).quantize(Decimal('0.000000001')) - - depth = 0 - reverse = False - while depth > z_cut: - - # Increase depth. Limit to z_cut. - depth -= depthpercut - 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 - # first point in the path, but it should be already - # at the first point if the tool is down (in the material). - # So, an extra G00 should show up but is inconsequential. - if type(geo) == LineString or type(geo) == LinearRing: - self.gcode += self.linear2gcode(geo, tolerance=tolerance, - zcut=depth, - up=False) - - # Ignore multi-pass for points. - elif type(geo) == Point: - self.gcode += self.point2gcode(geo) - break # Ignoring ... - - else: - log.warning("G-code generation not implemented for %s" % (str(type(geo)))) - - # Reverse coordinates if not a loop so we can continue - # cutting without returning to the beginning. - if type(geo) == LineString: - geo.coords = list(geo.coords)[::-1] - reverse = True - - # If geometry is reversed, revert. - if reverse: - if type(geo) == LineString: - geo.coords = list(geo.coords)[::-1] - - # Lift the tool - self.gcode += "G00 Z%.4f\n" % self.z_move - # self.gcode += "( End of path. )\n" - - # Did deletion at the beginning. - # Delete from index, update current location and continue. - #rti.delete(hits[0], geo.coords[0]) - #rti.delete(hits[0], geo.coords[-1]) - - current_pt = geo.coords[-1] - - # Next - pt, geo = storage.nearest(current_pt) - - except StopIteration: # Nothing found in storage. - pass - - log.debug("%s paths traced." % path_count) - - # Finish - self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting - self.gcode += "G00 X0Y0\n" - self.gcode += "M05\n" # Spindle stop - - @staticmethod - def codes_split(gline): - """ - Parses a line of G-Code such as "G01 X1234 Y987" into - a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0} - - :param gline: G-Code line string - :return: Dictionary with parsed line. - """ - - command = {} - - 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) - - return command - - def gcode_parse(self): - """ - G-Code parser (from self.gcode). Generates dictionary with - single-segment LineString's and "kind" indicating cut or travel, - fast or feedrate speed. - """ - - kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow - - # Results go here - geometry = [] - - # Last known instruction - current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0} - - # Current path: temporary storage until tool is - # lifted or lowered. - path = [(0, 0)] - - # Process every instruction - 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: - if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']: - log.warning("Non-orthogonal motion: From %s" % str(current)) - log.warning(" To: %s" % str(gobj)) - current['Z'] = gobj['Z'] - # Store the path into geometry and reset path - if len(path) > 1: - geometry.append({"geom": LineString(path), - "kind": kind}) - path = [path[-1]] # Start with the last point of last path. - - if 'G' in gobj: - current['G'] = int(gobj['G']) - - if 'X' in gobj or 'Y' in gobj: - - if 'X' in gobj: - x = gobj['X'] - else: - x = current['X'] - - if 'Y' in gobj: - y = gobj['Y'] - else: - y = current['Y'] - - kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow - - if current['Z'] > 0: - kind[0] = 'T' - if current['G'] > 0: - kind[1] = 'S' - - arcdir = [None, None, "cw", "ccw"] - if current['G'] in [0, 1]: # line - path.append((x, y)) - - if current['G'] in [2, 3]: # arc - 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) - path += arc(center, radius, start, stop, - arcdir[current['G']], - self.steps_per_circ) - - # Update current instruction - for code in gobj: - current[code] = gobj[code] - - # There might not be a change in height at the - # end, therefore, see here too if there is - # a final path. - if len(path) > 1: - geometry.append({"geom": LineString(path), - "kind": kind}) - - self.gcode_parsed = geometry - return geometry - - # def plot(self, tooldia=None, dpi=75, margin=0.1, - # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]}, - # alpha={"T": 0.3, "C": 1.0}): - # """ - # Creates a Matplotlib figure with a plot of the - # G-code job. - # """ - # if tooldia is None: - # tooldia = self.tooldia - # - # fig = Figure(dpi=dpi) - # ax = fig.add_subplot(111) - # ax.set_aspect(1) - # xmin, ymin, xmax, ymax = self.input_geometry_bounds - # ax.set_xlim(xmin-margin, xmax+margin) - # ax.set_ylim(ymin-margin, ymax+margin) - # - # if tooldia == 0: - # for geo in self.gcode_parsed: - # linespec = '--' - # linecolor = color[geo['kind'][0]][1] - # if geo['kind'][0] == 'C': - # linespec = 'k-' - # x, y = geo['geom'].coords.xy - # ax.plot(x, y, linespec, color=linecolor) - # else: - # for geo in self.gcode_parsed: - # poly = geo['geom'].buffer(tooldia/2.0) - # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0], - # edgecolor=color[geo['kind'][0]][1], - # alpha=alpha[geo['kind'][0]], zorder=2) - # ax.add_patch(patch) - # - # return fig - - def plot2(self, axes, tooldia=None, dpi=75, margin=0.1, - color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]}, - alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005): - """ - Plots the G-code job onto the given axes. - - :param axes: Matplotlib axes on which to plot. - :param tooldia: Tool diameter. - :param dpi: Not used! - :param margin: Not used! - :param color: Color specification. - :param alpha: Transparency specification. - :param tool_tolerance: Tolerance when drawing the toolshape. - :return: None - """ - path_num = 0 - - if tooldia is None: - tooldia = self.tooldia - - if tooldia == 0: - for geo in self.gcode_parsed: - linespec = '--' - linecolor = color[geo['kind'][0]][1] - if geo['kind'][0] == 'C': - linespec = 'k-' - x, y = geo['geom'].coords.xy - axes.plot(x, y, linespec, color=linecolor) - else: - for geo in self.gcode_parsed: - path_num += 1 - axes.annotate(str(path_num), xy=geo['geom'].coords[0], - xycoords='data') - - poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance) - patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0], - edgecolor=color[geo['kind'][0]][1], - alpha=alpha[geo['kind'][0]], zorder=2) - axes.add_patch(patch) - - def create_geometry(self): - # TODO: This takes forever. Too much data? - self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) - - def linear2gcode(self, linear, tolerance=0, down=True, up=True, - zcut=None, ztravel=None, downrate=None, - feedrate=None, cont=False): - """ - Generates G-code to cut along the linear feature. - - :param linear: The path to cut along. - :type: Shapely.LinearRing or Shapely.Linear String - :param tolerance: All points in the simplified object will be within the - tolerance distance of the original geometry. - :type tolerance: float - :return: G-code to cut along the linear feature. - :rtype: str - """ - - if zcut is None: - zcut = self.z_cut - - if ztravel is None: - ztravel = self.z_move - - if downrate is None: - downrate = self.zdownrate - - if feedrate is None: - feedrate = self.feedrate - - t = "G0%d " + CNCjob.defaults["coordinate_format"] + "\n" - - # Simplify paths? - if tolerance > 0: - target_linear = linear.simplify(tolerance) - else: - target_linear = linear - - gcode = "" - - path = list(target_linear.coords) - - # Move fast to 1st point - if not cont: - gcode += t % (0, path[0][0], path[0][1]) # Move to first point - - # Move down to cutting depth - if down: - # Different feedrate for vertical cut? - if self.zdownrate is not None: - gcode += "F%.2f\n" % downrate - gcode += "G01 Z%.4f\n" % zcut # Start cutting - gcode += "F%.2f\n" % feedrate # Restore feedrate - else: - gcode += "G01 Z%.4f\n" % zcut # Start cutting - - # Cutting... - for pt in path[1:]: - gcode += t % (1, pt[0], pt[1]) # Linear motion to point - - # Up to travelling height. - if up: - gcode += "G00 Z%.4f\n" % ztravel # Stop cutting - - return gcode - - def point2gcode(self, point): - gcode = "" - #t = "G0%d X%.4fY%.4f\n" - t = "G0%d " + CNCjob.defaults["coordinate_format"] + "\n" - path = list(point.coords) - gcode += t % (0, path[0][0], path[0][1]) # Move to first point - - if self.zdownrate is not None: - gcode += "F%.2f\n" % self.zdownrate - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - gcode += "F%.2f\n" % self.feedrate - else: - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - - gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting - return gcode - - def scale(self, factor): - """ - Scales all the geometry on the XY plane in the object by the - given factor. Tool sizes, feedrates, or Z-axis dimensions are - not altered. - - :param factor: Number by which to scale the object. - :type factor: float - :return: None - :rtype: None - """ - - for g in self.gcode_parsed: - g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0)) - - self.create_geometry() - - def offset(self, vect): - """ - Offsets all the geometry on the XY plane in the object by the - given vector. - - :param vect: (x, y) offset vector. - :type vect: tuple - :return: None - """ - dx, dy = vect - - for g in self.gcode_parsed: - g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy) - - self.create_geometry() - - def skew(self, angle_x=None, angle_y=None, point=None): - """ - Shear/Skew the geometries of an object by angles along x and y dimensions. - - Parameters - ---------- - angle_x, angle_y : float, float - The shear angle(s) for the x and y axes respectively. These can be - specified in either degrees (default) or radians by setting - use_radians=True. - point: tupple of coordinates . Origin for skew. - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - - if angle_y is None: - angle_y = 0.0 - if angle_x is None: - angle_x = 0.0 - if point == None: - for g in self.gcode_parsed: - g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, - origin=(0, 0)) - else: - for g in self.gcode_parsed: - g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, - origin=point) - - self.create_geometry() - - def rotate(self, angle, point=None): - """ - Rotate the geometrys of an object by an given angle around the coordinates of the 'point' - :param angle: - :param point: - :return: - """ - if point is None: - for g in self.gcode_parsed: - g['geom'] = affinity.rotate(g['geom'], angle, origin='center') - else: - px, py = point - for g in self.gcode_parsed: - g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py)) - - self.create_geometry() - - def mirror(self, axis, point=None): - """ - Mirror the geometrys of an object around the coordinates of the 'point' - :param axis: X or Y - :param point: tupple of coordinates - :return: - """ - if point is None: - return - else: - px, py = point - - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - for g in self.gcode_parsed: - g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py)) - - self.create_geometry() - return - - 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 -# ymin = Inf -# xmax = -Inf -# ymax = -Inf -# -# #print "Getting bounds of:", str(geometry_set) -# for gs in geometry_set: -# try: -# gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds() -# xmin = min([xmin, gxmin]) -# ymin = min([ymin, gymin]) -# xmax = max([xmax, gxmax]) -# ymax = max([ymax, gymax]) -# except: -# print "DEV WARNING: Tried to get bounds of empty geometry." -# -# return [xmin, ymin, xmax, ymax] - -def get_bounds(geometry_list): - xmin = Inf - ymin = Inf - xmax = -Inf - ymax = -Inf - - #print "Getting bounds of:", str(geometry_set) - for gs in geometry_list: - try: - gxmin, gymin, gxmax, gymax = gs.bounds() - xmin = min([xmin, gxmin]) - ymin = min([ymin, gymin]) - xmax = max([xmax, gxmax]) - ymax = max([ymax, gymax]) - except: - log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.") - - return [xmin, ymin, xmax, ymax] - - -def arc(center, radius, start, stop, direction, steps_per_circ): - """ - Creates a list of point along the specified arc. - - :param center: Coordinates of the center [x, y] - :type center: list - :param radius: Radius of the arc. - :type radius: float - :param start: Starting angle in radians - :type start: float - :param stop: End angle in radians - :type stop: float - :param direction: Orientation of the arc, "CW" or "CCW" - :type direction: string - :param steps_per_circ: Number of straight line segments to - represent a circle. - :type steps_per_circ: int - :return: The desired arc, as list of tuples - :rtype: list - """ - # TODO: Resolution should be established by maximum error from the exact arc. - - da_sign = {"cw": -1.0, "ccw": 1.0} - points = [] - if direction == "ccw" and stop <= start: - stop += 2 * pi - if direction == "cw" and stop >= start: - stop -= 2 * pi - - angle = abs(stop - start) - - #angle = stop-start - steps = max([int(ceil(angle / (2 * pi) * steps_per_circ)), 2]) - delta_angle = da_sign[direction] * angle * 1.0 / steps - for i in range(steps + 1): - theta = start + delta_angle * i - points.append((center[0] + radius * cos(theta), center[1] + radius * sin(theta))) - return points - - -def arc2(p1, p2, center, direction, steps_per_circ): - r = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2) - start = arctan2(p1[1] - center[1], p1[0] - center[0]) - stop = arctan2(p2[1] - center[1], p2[0] - center[0]) - return arc(center, r, start, stop, direction, steps_per_circ) - - -def arc_angle(start, stop, direction): - if direction == "ccw" and stop <= start: - stop += 2 * pi - if direction == "cw" and stop >= start: - stop -= 2 * pi - - angle = abs(stop - start) - return angle - - -# def find_polygon(poly, point): -# """ -# Find an object that object.contains(Point(point)) in -# poly, which can can be iterable, contain iterable of, or -# be itself an implementer of .contains(). -# -# :param poly: See description -# :return: Polygon containing point or None. -# """ -# -# if poly is None: -# return None -# -# try: -# for sub_poly in poly: -# p = find_polygon(sub_poly, point) -# if p is not None: -# return p -# except TypeError: -# try: -# if poly.contains(Point(point)): -# return poly -# except AttributeError: -# return None -# -# return None - - -def to_dict(obj): - """ - Makes the following types into serializable form: - - * ApertureMacro - * BaseGeometry - - :param obj: Shapely geometry. - :type obj: BaseGeometry - :return: Dictionary with serializable form if ``obj`` was - BaseGeometry or ApertureMacro, otherwise returns ``obj``. - """ - if isinstance(obj, ApertureMacro): - return { - "__class__": "ApertureMacro", - "__inst__": obj.to_dict() - } - if isinstance(obj, BaseGeometry): - return { - "__class__": "Shply", - "__inst__": sdumps(obj) - } - return obj - - -def dict2obj(d): - """ - Default deserializer. - - :param d: Serializable dictionary representation of an object - to be reconstructed. - :return: Reconstructed object. - """ - if '__class__' in d and '__inst__' in d: - if d['__class__'] == "Shply": - return sloads(d['__inst__']) - if d['__class__'] == "ApertureMacro": - am = ApertureMacro() - am.from_dict(d['__inst__']) - return am - return d - else: - return d - - -def plotg(geo, solid_poly=False, color="black"): - try: - _ = iter(geo) - except: - geo = [geo] - - for g in geo: - if type(g) == Polygon: - if solid_poly: - patch = PolygonPatch(g, - facecolor="#BBF268", - edgecolor="#006E20", - alpha=0.75, - zorder=2) - ax = subplot(111) - ax.add_patch(patch) - else: - x, y = g.exterior.coords.xy - plot(x, y, color=color) - for ints in g.interiors: - x, y = ints.coords.xy - plot(x, y, color=color) - continue - - if type(g) == LineString or type(g) == LinearRing: - x, y = g.coords.xy - plot(x, y, color=color) - continue - - if type(g) == Point: - x, y = g.coords.xy - plot(x, y, 'o') - continue - - try: - _ = iter(g) - plotg(g, color=color) - except: - log.error("Cannot plot: " + str(type(g))) - continue - - -def parse_gerber_number(strnumber, frac_digits): - """ - Parse a single number of Gerber coordinates. - - :param strnumber: String containing a number in decimal digits - from a coordinate data block, possibly with a leading sign. - :type strnumber: str - :param frac_digits: Number of digits used for the fractional - part of the number - :type frac_digits: int - :return: The number in floating point. - :rtype: float - """ - return int(strnumber) * (10 ** (-frac_digits)) - - -# def voronoi(P): -# """ -# Returns a list of all edges of the voronoi diagram for the given input points. -# """ -# delauny = Delaunay(P) -# triangles = delauny.points[delauny.vertices] -# -# circum_centers = np.array([triangle_csc(tri) for tri in triangles]) -# long_lines_endpoints = [] -# -# lineIndices = [] -# for i, triangle in enumerate(triangles): -# circum_center = circum_centers[i] -# for j, neighbor in enumerate(delauny.neighbors[i]): -# if neighbor != -1: -# lineIndices.append((i, neighbor)) -# else: -# ps = triangle[(j+1)%3] - triangle[(j-1)%3] -# ps = np.array((ps[1], -ps[0])) -# -# middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5 -# di = middle - triangle[j] -# -# ps /= np.linalg.norm(ps) -# di /= np.linalg.norm(di) -# -# if np.dot(di, ps) < 0.0: -# ps *= -1000.0 -# else: -# ps *= 1000.0 -# -# long_lines_endpoints.append(circum_center + ps) -# lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1)) -# -# vertices = np.vstack((circum_centers, long_lines_endpoints)) -# -# # filter out any duplicate lines -# lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2) -# lineIndicesTupled = [tuple(row) for row in lineIndicesSorted] -# lineIndicesUnique = np.unique(lineIndicesTupled) -# -# return vertices, lineIndicesUnique -# -# -# def triangle_csc(pts): -# rows, cols = pts.shape -# -# A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))], -# [np.ones((1, rows)), np.zeros((1, 1))]]) -# -# b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1)))) -# x = np.linalg.solve(A,b) -# bary_coords = x[:-1] -# return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0) -# -# -# def voronoi_cell_lines(points, vertices, lineIndices): -# """ -# Returns a mapping from a voronoi cell to its edges. -# -# :param points: shape (m,2) -# :param vertices: shape (n,2) -# :param lineIndices: shape (o,2) -# :rtype: dict point index -> list of shape (n,2) with vertex indices -# """ -# kd = KDTree(points) -# -# cells = collections.defaultdict(list) -# for i1, i2 in lineIndices: -# v1, v2 = vertices[i1], vertices[i2] -# mid = (v1+v2)/2 -# _, (p1Idx, p2Idx) = kd.query(mid, 2) -# cells[p1Idx].append((i1, i2)) -# cells[p2Idx].append((i1, i2)) -# -# return cells -# -# -# def voronoi_edges2polygons(cells): -# """ -# Transforms cell edges into polygons. -# -# :param cells: as returned from voronoi_cell_lines -# :rtype: dict point index -> list of vertex indices which form a polygon -# """ -# -# # first, close the outer cells -# for pIdx, lineIndices_ in cells.items(): -# dangling_lines = [] -# for i1, i2 in lineIndices_: -# connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_) -# assert 1 <= len(connections) <= 2 -# if len(connections) == 1: -# dangling_lines.append((i1, i2)) -# assert len(dangling_lines) in [0, 2] -# if len(dangling_lines) == 2: -# (i11, i12), (i21, i22) = dangling_lines -# -# # determine which line ends are unconnected -# connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_) -# i11Unconnected = len(connected) == 0 -# -# connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_) -# i21Unconnected = len(connected) == 0 -# -# startIdx = i11 if i11Unconnected else i12 -# endIdx = i21 if i21Unconnected else i22 -# -# cells[pIdx].append((startIdx, endIdx)) -# -# # then, form polygons by storing vertex indices in (counter-)clockwise order -# polys = dict() -# for pIdx, lineIndices_ in cells.items(): -# # get a directed graph which contains both directions and arbitrarily follow one of both -# directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_] -# directedGraphMap = collections.defaultdict(list) -# for (i1, i2) in directedGraph: -# directedGraphMap[i1].append(i2) -# orderedEdges = [] -# currentEdge = directedGraph[0] -# while len(orderedEdges) < len(lineIndices_): -# i1 = currentEdge[1] -# i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1] -# nextEdge = (i1, i2) -# orderedEdges.append(nextEdge) -# currentEdge = nextEdge -# -# polys[pIdx] = [i1 for (i1, i2) in orderedEdges] -# -# return polys -# -# -# def voronoi_polygons(points): -# """ -# Returns the voronoi polygon for each input point. -# -# :param points: shape (n,2) -# :rtype: list of n polygons where each polygon is an array of vertices -# """ -# vertices, lineIndices = voronoi(points) -# cells = voronoi_cell_lines(points, vertices, lineIndices) -# polys = voronoi_edges2polygons(cells) -# polylist = [] -# for i in xrange(len(points)): -# poly = vertices[np.asarray(polys[i])] -# polylist.append(poly) -# return polylist -# -# -# class Zprofile: -# def __init__(self): -# -# # data contains lists of [x, y, z] -# self.data = [] -# -# # Computed voronoi polygons (shapely) -# self.polygons = [] -# pass -# -# def plot_polygons(self): -# axes = plt.subplot(1, 1, 1) -# -# plt.axis([-0.05, 1.05, -0.05, 1.05]) -# -# for poly in self.polygons: -# p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3) -# axes.add_patch(p) -# -# def init_from_csv(self, filename): -# pass -# -# def init_from_string(self, zpstring): -# pass -# -# def init_from_list(self, zplist): -# self.data = zplist -# -# def generate_polygons(self): -# self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))] -# -# def normalize(self, origin): -# pass -# -# def paste(self, path): -# """ -# Return a list of dictionaries containing the parts of the original -# path and their z-axis offset. -# """ -# -# # At most one region/polygon will contain the path -# containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)] -# -# if len(containing) > 0: -# return [{"path": path, "z": self.data[containing[0]][2]}] -# -# # All region indexes that intersect with the path -# crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)] -# -# return [{"path": path.intersection(self.polygons[i]), -# "z": self.data[i][2]} for i in crossing] - - -def autolist(obj): - try: - _ = iter(obj) - return obj - except TypeError: - return [obj] - - -def three_point_circle(p1, p2, p3): - """ - Computes the center and radius of a circle from - 3 points on its circumference. - - :param p1: Point 1 - :param p2: Point 2 - :param p3: Point 3 - :return: center, radius - """ - # Midpoints - a1 = (p1 + p2) / 2.0 - a2 = (p2 + p3) / 2.0 - - # Normals - b1 = dot((p2 - p1), array([[0, -1], [1, 0]], dtype=float32)) - b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32)) - - # Params - T = solve(transpose(array([-b1, b2])), a1 - a2) - - # Center - center = a1 + b1 * T[0] - - # Radius - radius = norm(center - p1) - - return center, radius, T[0] - - -def distance(pt1, pt2): - return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) - - -class FlatCAMRTree(object): - """ - Indexes geometry (Any object with "cooords" property containing - a list of tuples with x, y values). Objects are indexed by - all their points by default. To index by arbitrary points, - override self.points2obj. - """ - - def __init__(self): - # Python RTree Index - self.rti = rtindex.Index() - - ## Track object-point relationship - # Each is list of points in object. - self.obj2points = [] - - # Index is index in rtree, value is index of - # object in obj2points. - self.points2obj = [] - - self.get_points = lambda go: go.coords - - def grow_obj2points(self, idx): - """ - Increases the size of self.obj2points to fit - idx + 1 items. - - :param idx: Index to fit into list. - :return: None - """ - if len(self.obj2points) > idx: - # len == 2, idx == 1, ok. - return - else: - # len == 2, idx == 2, need 1 more. - # range(2, 3) - for i in range(len(self.obj2points), idx + 1): - self.obj2points.append([]) - - def insert(self, objid, obj): - self.grow_obj2points(objid) - self.obj2points[objid] = [] - - for pt in self.get_points(obj): - self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid) - self.obj2points[objid].append(len(self.points2obj)) - self.points2obj.append(objid) - - def remove_obj(self, objid, obj): - # Use all ptids to delete from index - for i, pt in enumerate(self.get_points(obj)): - self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1])) - - def nearest(self, pt): - """ - Will raise StopIteration if no items are found. - - :param pt: - :return: - """ - return next(self.rti.nearest(pt, objects=True)) - - -class FlatCAMRTreeStorage(FlatCAMRTree): - """ - Just like FlatCAMRTree it indexes geometry, but also serves - as storage for the geometry. - """ - - def __init__(self): - super(FlatCAMRTreeStorage, self).__init__() - - self.objects = [] - - # Optimization attempt! - self.indexes = {} - - def insert(self, obj): - self.objects.append(obj) - idx = len(self.objects) - 1 - - # Note: Shapely objects are not hashable any more, althought - # there seem to be plans to re-introduce the feature in - # version 2.0. For now, we will index using the object's id, - # but it's important to remember that shapely geometry is - # mutable, ie. it can be modified to a totally different shape - # and continue to have the same id. - # self.indexes[obj] = idx - self.indexes[id(obj)] = idx - - super(FlatCAMRTreeStorage, self).insert(idx, obj) - - #@profile - def remove(self, obj): - # See note about self.indexes in insert(). - # objidx = self.indexes[obj] - objidx = self.indexes[id(obj)] - - # Remove from list - self.objects[objidx] = None - - # Remove from index - self.remove_obj(objidx, obj) - - def get_objects(self): - return (o for o in self.objects if o is not None) - - def nearest(self, pt): - """ - Returns the nearest matching points and the object - it belongs to. - - :param pt: Query point. - :return: (match_x, match_y), Object owner of - matching point. - :rtype: tuple - """ - tidx = super(FlatCAMRTreeStorage, self).nearest(pt) - return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object] - - -# class myO: -# def __init__(self, coords): -# self.coords = coords -# -# -# def test_rti(): -# -# o1 = myO([(0, 0), (0, 1), (1, 1)]) -# o2 = myO([(2, 0), (2, 1), (2, 1)]) -# o3 = myO([(2, 0), (2, 1), (3, 1)]) -# -# os = [o1, o2] -# -# idx = FlatCAMRTree() -# -# for o in range(len(os)): -# idx.insert(o, os[o]) -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] -# -# idx.remove_obj(0, o1) -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] -# -# idx.remove_obj(1, o2) -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] -# -# -# def test_rtis(): -# -# o1 = myO([(0, 0), (0, 1), (1, 1)]) -# o2 = myO([(2, 0), (2, 1), (2, 1)]) -# o3 = myO([(2, 0), (2, 1), (3, 1)]) -# -# os = [o1, o2] -# -# idx = FlatCAMRTreeStorage() -# -# for o in range(len(os)): -# idx.insert(os[o]) -# -# #os = None -# #o1 = None -# #o2 = None -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] -# -# idx.remove(idx.nearest((2,0))[1]) -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] -# -# idx.remove(idx.nearest((0,0))[1]) -# -# print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)] diff --git a/flatcam b/flatcam deleted file mode 100755 index fb3daa8c..00000000 --- a/flatcam +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -############################################################ - -import sys -from PyQt4 import QtGui -from PyQt4 import QtCore -from FlatCAMApp import App - - -def debug_trace(): - """ - 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) -QtCore.QDir.setSearchPaths("share", str(("share", "share/flatcam", "/usr/share/flatcam"))); -fc = App() -sys.exit(app.exec_()) diff --git a/flatcam.desktop b/flatcam.desktop deleted file mode 100644 index 920f9c24..00000000 --- a/flatcam.desktop +++ /dev/null @@ -1,8 +0,0 @@ -[Desktop Entry] -Name=FlatCAM -Comment=2D Computer-Aided PCB Manufacturing -Exec=flatcam -Icon=flatcam -Keywords=cam;pcb;gerber -Terminal=false -Type=Application \ No newline at end of file diff --git a/make_win32.py b/make_win32.py deleted file mode 100644 index 5837fadd..00000000 --- a/make_win32.py +++ /dev/null @@ -1,70 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Juan Pablo Caram (c) # -# Date: 2/5/2014 # -# MIT Licence # -# # -# Creates a portlable copy of FlatCAM, including Python # -# itself and all dependencies. # -# # -# This is not an aid to install FlatCAM from source on # -# Windows platforms. It is only useful when FlatCAM is up # -# and running and ready to be packaged. # -############################################################ - -# Files not needed: Qt, tk.dll, tcl.dll, tk/, tcl/, vtk/, -# scipy.lib.lapack.flapack.pyd, scipy.lib.blas.fblas.pyd, -# numpy.core._dotblas.pyd, scipy.sparse.sparsetools._bsr.pyd, -# scipy.sparse.sparsetools._csr.pyd, scipy.sparse.sparsetools._csc.pyd, -# scipy.sparse.sparsetools._coo.pyd - -import os, site, sys -from cx_Freeze import setup, Executable - -# this is done to solve the tkinter not being found (Python3) -# still the DLL's need to be copied to the lib folder but the script can't do it -PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__)) -os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6') -os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6') - -## Get the site-package folder, not everybody will install -## Python into C:\PythonXX -site_dir = site.getsitepackages()[1] - -include_files = [] -include_files.append((os.path.join(site_dir, "shapely"), "shapely")) -include_files.append((os.path.join(site_dir, "svg"), "svg")) -include_files.append((os.path.join(site_dir, "svg/path"), "svg")) -include_files.append((os.path.join(site_dir, "matplotlib"), "matplotlib")) -include_files.append(("share", "share")) -include_files.append((os.path.join(site_dir, "rtree"), "rtree")) -include_files.append(("README.md", "README.md")) -include_files.append(("LICENSE", "LICENSE")) - -base = None - -## Lets not open the console while running the app -if sys.platform == "win32": - base = "Win32GUI" - -buildOptions = dict( - include_files=include_files, - # excludes=['PyQt4', 'tk', 'tcl'] - excludes=['scipy.lib.lapack.flapack.pyd', - 'scipy.lib.blas.fblas.pyd', - 'QtOpenGL4.dll'] -) - -print(("INCLUDE_FILES", include_files)) - -exec(compile(open('clean.py').read(), 'clean.py', 'exec')) - -setup( - name="FlatCAM", - author="Juan Pablo Caram", - version="8.5", - description="FlatCAM: 2D Computer Aided PCB Manufacturing", - options=dict(build_exe=buildOptions), - executables=[Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base)] -) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6e24281e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# This file contains python only requirements to be installed with pip -# Python pacakges that cannot be installed with pip (e.g. PyQT4) are not included. -# Usage: pip install -r requirements.txt -numpy>=1.8 -matplotlib>=1.3.1 -shapely>=1.3 -simplejson -rtree -scipy -svg.path diff --git a/setup.py b/setup.py deleted file mode 100644 index 38c8466e..00000000 --- a/setup.py +++ /dev/null @@ -1,75 +0,0 @@ -############################################################ -# FlatCAM: 2D Post-processing for Manufacturing # -# http://flatcam.org # -# Author: Damian Wrobel # -# Date: 05/23/2017 # -# MIT Licence # -# A setuptools based setup module # -############################################################ - -from setuptools import setup -import glob - -setup( - name='FlatCAM', - - version='8.5', - - description='2D Computer-Aided PCB Manufacturing', - - long_description=('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.'), - - url='http://flatcam.org/', - - author='Juan Pablo Caram', - - license='MIT', - - packages=[ - 'descartes', - 'tclCommands' - ], - - py_modules=[ - "camlib", - "DblSidedTool", - "FlatCAMApp", - "FlatCAMCommon", - "FlatCAMDraw", - "FlatCAMGUI", - "FlatCAMObj", - "FlatCAMProcess", - "FlatCAMShell", - "FlatCAMTool", - "FlatCAMVersion", - "FlatCAMWorker", - "GUIElements", - "MeasurementTool", - "ObjectCollection", - "ObjectUI", - "PlotCanvas", - "svgparse", - "termwidget" - ], - - install_requires=[ - 'simplejson', - 'numpy>=1.8', - 'scipy', - 'matplotlib>=1.3.1', - 'shapely>=1.3' - 'rtree', - 'svg.path' - ], - - include_package_data=True, - - data_files=[ - ('share/flatcam', glob.glob('share/*')) - ], - - scripts=['flatcam'] -) diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh deleted file mode 100755 index db1b7b1a..00000000 --- a/setup_ubuntu.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -e -# "-e" exists on first error. - -apt-get install libpng-dev \ - libfreetype6 \ - libfreetype6-dev \ - python-dev \ - python-simplejson \ - python-qt4 \ - python-numpy \ - python-scipy \ - python-matplotlib \ - libgeos-dev \ - python-shapely \ - python-pip \ - libspatialindex-dev -easy_install -U distribute -pip install --upgrade matplotlib Shapely -pip install rtree -pip install svg.path diff --git a/share/active.gif b/share/active.gif deleted file mode 100644 index b2cb55d7..00000000 Binary files a/share/active.gif and /dev/null differ diff --git a/share/arc16.png b/share/arc16.png deleted file mode 100644 index f61e25b5..00000000 Binary files a/share/arc16.png and /dev/null differ diff --git a/share/arc24.png b/share/arc24.png deleted file mode 100644 index 17c0f466..00000000 Binary files a/share/arc24.png and /dev/null differ diff --git a/share/arc32.png b/share/arc32.png deleted file mode 100644 index cb4bdef2..00000000 Binary files a/share/arc32.png and /dev/null differ diff --git a/share/blocked16.png b/share/blocked16.png deleted file mode 100644 index bec6ecff..00000000 Binary files a/share/blocked16.png and /dev/null differ diff --git a/share/buffer16.png b/share/buffer16.png deleted file mode 100644 index 02bae3cb..00000000 Binary files a/share/buffer16.png and /dev/null differ diff --git a/share/bug16.png b/share/bug16.png deleted file mode 100644 index ba62ffd9..00000000 Binary files a/share/bug16.png and /dev/null differ diff --git a/share/cancel_edit16.png b/share/cancel_edit16.png deleted file mode 100644 index 6ee2bae7..00000000 Binary files a/share/cancel_edit16.png and /dev/null differ diff --git a/share/cancel_edit32.png b/share/cancel_edit32.png deleted file mode 100644 index f087c718..00000000 Binary files a/share/cancel_edit32.png and /dev/null differ diff --git a/share/circle32.png b/share/circle32.png deleted file mode 100644 index 848a67ee..00000000 Binary files a/share/circle32.png and /dev/null differ diff --git a/share/clear_plot16.png b/share/clear_plot16.png deleted file mode 100644 index 4f77d00c..00000000 Binary files a/share/clear_plot16.png and /dev/null differ diff --git a/share/clear_plot32.png b/share/clear_plot32.png deleted file mode 100644 index a48b61b9..00000000 Binary files a/share/clear_plot32.png and /dev/null differ diff --git a/share/cnc16.png b/share/cnc16.png deleted file mode 100644 index ee0fb0aa..00000000 Binary files a/share/cnc16.png and /dev/null differ diff --git a/share/cnc32.png b/share/cnc32.png deleted file mode 100644 index a317cf10..00000000 Binary files a/share/cnc32.png and /dev/null differ diff --git a/share/copy16.png b/share/copy16.png deleted file mode 100644 index 37d2c519..00000000 Binary files a/share/copy16.png and /dev/null differ diff --git a/share/copy32.png b/share/copy32.png deleted file mode 100644 index 1666def5..00000000 Binary files a/share/copy32.png and /dev/null differ diff --git a/share/corner32.png b/share/corner32.png deleted file mode 100644 index aefe64ea..00000000 Binary files a/share/corner32.png and /dev/null differ diff --git a/share/cutpath16.png b/share/cutpath16.png deleted file mode 100644 index 8693d35a..00000000 Binary files a/share/cutpath16.png and /dev/null differ diff --git a/share/cutpath24.png b/share/cutpath24.png deleted file mode 100644 index 1226f5c2..00000000 Binary files a/share/cutpath24.png and /dev/null differ diff --git a/share/cutpath32.png b/share/cutpath32.png deleted file mode 100644 index 9a59e86a..00000000 Binary files a/share/cutpath32.png and /dev/null differ diff --git a/share/delete32.png b/share/delete32.png deleted file mode 100644 index da05e045..00000000 Binary files a/share/delete32.png and /dev/null differ diff --git a/share/deleteshape16.png b/share/deleteshape16.png deleted file mode 100644 index cb672fa2..00000000 Binary files a/share/deleteshape16.png and /dev/null differ diff --git a/share/deleteshape24.png b/share/deleteshape24.png deleted file mode 100644 index 8b2cdda9..00000000 Binary files a/share/deleteshape24.png and /dev/null differ diff --git a/share/deleteshape32.png b/share/deleteshape32.png deleted file mode 100644 index 822c423f..00000000 Binary files a/share/deleteshape32.png and /dev/null differ diff --git a/share/doubleside16.png b/share/doubleside16.png deleted file mode 100644 index 5dcc3b4a..00000000 Binary files a/share/doubleside16.png and /dev/null differ diff --git a/share/doubleside32.png b/share/doubleside32.png deleted file mode 100644 index 001be1e7..00000000 Binary files a/share/doubleside32.png and /dev/null differ diff --git a/share/drill16.png b/share/drill16.png deleted file mode 100644 index d7091621..00000000 Binary files a/share/drill16.png and /dev/null differ diff --git a/share/drill32.png b/share/drill32.png deleted file mode 100644 index 1b832c15..00000000 Binary files a/share/drill32.png and /dev/null differ diff --git a/share/edit16.png b/share/edit16.png deleted file mode 100644 index 9b64793d..00000000 Binary files a/share/edit16.png and /dev/null differ diff --git a/share/edit32.png b/share/edit32.png deleted file mode 100644 index 5e29fe17..00000000 Binary files a/share/edit32.png and /dev/null differ diff --git a/share/edit_ok16.png b/share/edit_ok16.png deleted file mode 100644 index 5c04718a..00000000 Binary files a/share/edit_ok16.png and /dev/null differ diff --git a/share/edit_ok32.png b/share/edit_ok32.png deleted file mode 100644 index 0674a1a8..00000000 Binary files a/share/edit_ok32.png and /dev/null differ diff --git a/share/file16.png b/share/file16.png deleted file mode 100644 index 783b871e..00000000 Binary files a/share/file16.png and /dev/null differ diff --git a/share/flatcam_icon128.png b/share/flatcam_icon128.png deleted file mode 100644 index f313bdda..00000000 Binary files a/share/flatcam_icon128.png and /dev/null differ diff --git a/share/flatcam_icon128_inv.png b/share/flatcam_icon128_inv.png deleted file mode 100644 index 60782f32..00000000 Binary files a/share/flatcam_icon128_inv.png and /dev/null differ diff --git a/share/flatcam_icon16.ico b/share/flatcam_icon16.ico deleted file mode 100644 index 3c109e37..00000000 Binary files a/share/flatcam_icon16.ico and /dev/null differ diff --git a/share/flatcam_icon16.png b/share/flatcam_icon16.png deleted file mode 100644 index 6a88a971..00000000 Binary files a/share/flatcam_icon16.png and /dev/null differ diff --git a/share/flatcam_icon24.png b/share/flatcam_icon24.png deleted file mode 100644 index af7ed008..00000000 Binary files a/share/flatcam_icon24.png and /dev/null differ diff --git a/share/flatcam_icon256.ico b/share/flatcam_icon256.ico deleted file mode 100644 index b9a6b7bf..00000000 Binary files a/share/flatcam_icon256.ico and /dev/null differ diff --git a/share/flatcam_icon256.png b/share/flatcam_icon256.png deleted file mode 100644 index 7563feaa..00000000 Binary files a/share/flatcam_icon256.png and /dev/null differ diff --git a/share/flatcam_icon32.ico b/share/flatcam_icon32.ico deleted file mode 100644 index b8509b32..00000000 Binary files a/share/flatcam_icon32.ico and /dev/null differ diff --git a/share/flatcam_icon32.png b/share/flatcam_icon32.png deleted file mode 100644 index 199ff685..00000000 Binary files a/share/flatcam_icon32.png and /dev/null differ diff --git a/share/flatcam_icon48.ico b/share/flatcam_icon48.ico deleted file mode 100644 index 84c1c5e3..00000000 Binary files a/share/flatcam_icon48.ico and /dev/null differ diff --git a/share/flatcam_icon48.png b/share/flatcam_icon48.png deleted file mode 100644 index d7a72c0f..00000000 Binary files a/share/flatcam_icon48.png and /dev/null differ diff --git a/share/flipx.png b/share/flipx.png deleted file mode 100644 index 84d6a0a6..00000000 Binary files a/share/flipx.png and /dev/null differ diff --git a/share/flipy.png b/share/flipy.png deleted file mode 100644 index 21b7b2b5..00000000 Binary files a/share/flipy.png and /dev/null differ diff --git a/share/floppy16.png b/share/floppy16.png deleted file mode 100644 index 040b67b9..00000000 Binary files a/share/floppy16.png and /dev/null differ diff --git a/share/floppy32.png b/share/floppy32.png deleted file mode 100644 index 643f8228..00000000 Binary files a/share/floppy32.png and /dev/null differ diff --git a/share/folder16.png b/share/folder16.png deleted file mode 100644 index 22a50371..00000000 Binary files a/share/folder16.png and /dev/null differ diff --git a/share/gear32.png b/share/gear32.png deleted file mode 100644 index 27b5fe69..00000000 Binary files a/share/gear32.png and /dev/null differ diff --git a/share/gear48.png b/share/gear48.png deleted file mode 100644 index 0dbcb2db..00000000 Binary files a/share/gear48.png and /dev/null differ diff --git a/share/geometry16.png b/share/geometry16.png deleted file mode 100644 index c7845323..00000000 Binary files a/share/geometry16.png and /dev/null differ diff --git a/share/geometry32.png b/share/geometry32.png deleted file mode 100644 index a4ef770a..00000000 Binary files a/share/geometry32.png and /dev/null differ diff --git a/share/globe16.png b/share/globe16.png deleted file mode 100644 index c28de5aa..00000000 Binary files a/share/globe16.png and /dev/null differ diff --git a/share/graylight12.png b/share/graylight12.png deleted file mode 100644 index b4dcf780..00000000 Binary files a/share/graylight12.png and /dev/null differ diff --git a/share/greenlight12.png b/share/greenlight12.png deleted file mode 100644 index d15ca432..00000000 Binary files a/share/greenlight12.png and /dev/null differ diff --git a/share/grid16.png b/share/grid16.png deleted file mode 100644 index 5d6ed321..00000000 Binary files a/share/grid16.png and /dev/null differ diff --git a/share/grid32.png b/share/grid32.png deleted file mode 100644 index a4f9bb60..00000000 Binary files a/share/grid32.png and /dev/null differ diff --git a/share/home16.png b/share/home16.png deleted file mode 100644 index 914ff7c4..00000000 Binary files a/share/home16.png and /dev/null differ diff --git a/share/info16.png b/share/info16.png deleted file mode 100644 index 7ad1c70e..00000000 Binary files a/share/info16.png and /dev/null differ diff --git a/share/intersection16.png b/share/intersection16.png deleted file mode 100644 index e6298995..00000000 Binary files a/share/intersection16.png and /dev/null differ diff --git a/share/intersection24.png b/share/intersection24.png deleted file mode 100644 index a756e97c..00000000 Binary files a/share/intersection24.png and /dev/null differ diff --git a/share/intersection32.png b/share/intersection32.png deleted file mode 100644 index 0e3077f6..00000000 Binary files a/share/intersection32.png and /dev/null differ diff --git a/share/join16.png b/share/join16.png deleted file mode 100644 index be9eb596..00000000 Binary files a/share/join16.png and /dev/null differ diff --git a/share/join32.png b/share/join32.png deleted file mode 100644 index fe731b48..00000000 Binary files a/share/join32.png and /dev/null differ diff --git a/share/machine16.png b/share/machine16.png deleted file mode 100644 index 21c44ac5..00000000 Binary files a/share/machine16.png and /dev/null differ diff --git a/share/measure16.png b/share/measure16.png deleted file mode 100644 index 82df1c10..00000000 Binary files a/share/measure16.png and /dev/null differ diff --git a/share/measure32.png b/share/measure32.png deleted file mode 100644 index fff4e305..00000000 Binary files a/share/measure32.png and /dev/null differ diff --git a/share/move32.png b/share/move32.png deleted file mode 100644 index 839ecffc..00000000 Binary files a/share/move32.png and /dev/null differ diff --git a/share/new_geo16.png b/share/new_geo16.png deleted file mode 100644 index fd495e80..00000000 Binary files a/share/new_geo16.png and /dev/null differ diff --git a/share/new_geo32.png b/share/new_geo32.png deleted file mode 100644 index 6fa8c214..00000000 Binary files a/share/new_geo32.png and /dev/null differ diff --git a/share/paint16.png b/share/paint16.png deleted file mode 100644 index 63cf5398..00000000 Binary files a/share/paint16.png and /dev/null differ diff --git a/share/path32.png b/share/path32.png deleted file mode 100644 index d7627686..00000000 Binary files a/share/path32.png and /dev/null differ diff --git a/share/pointer.svg b/share/pointer.svg deleted file mode 100644 index 3468d2c1..00000000 --- a/share/pointer.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - 11 - computer - - - - - - - - - - - - - - - - - - - image/svg+xml - - - en - - - - - - - - - - - - - - - - - - - - - diff --git a/share/pointer32.png b/share/pointer32.png deleted file mode 100644 index d2b030c4..00000000 Binary files a/share/pointer32.png and /dev/null differ diff --git a/share/polygon32.png b/share/polygon32.png deleted file mode 100644 index cee65d68..00000000 Binary files a/share/polygon32.png and /dev/null differ diff --git a/share/power16.png b/share/power16.png deleted file mode 100644 index 0f41e114..00000000 Binary files a/share/power16.png and /dev/null differ diff --git a/share/project16.png b/share/project16.png deleted file mode 100644 index f5129e47..00000000 Binary files a/share/project16.png and /dev/null differ diff --git a/share/rectangle32.png b/share/rectangle32.png deleted file mode 100644 index fc933036..00000000 Binary files a/share/rectangle32.png and /dev/null differ diff --git a/share/recycle16.png b/share/recycle16.png deleted file mode 100644 index aa034bf0..00000000 Binary files a/share/recycle16.png and /dev/null differ diff --git a/share/redlight12.png b/share/redlight12.png deleted file mode 100644 index 9a1817e3..00000000 Binary files a/share/redlight12.png and /dev/null differ diff --git a/share/replot16.png b/share/replot16.png deleted file mode 100644 index 7ca80920..00000000 Binary files a/share/replot16.png and /dev/null differ diff --git a/share/replot32.png b/share/replot32.png deleted file mode 100644 index e17c2388..00000000 Binary files a/share/replot32.png and /dev/null differ diff --git a/share/rotate.png b/share/rotate.png deleted file mode 100644 index 1cbdfa42..00000000 Binary files a/share/rotate.png and /dev/null differ diff --git a/share/shell16.png b/share/shell16.png deleted file mode 100644 index 573f4b93..00000000 Binary files a/share/shell16.png and /dev/null differ diff --git a/share/shell32.png b/share/shell32.png deleted file mode 100644 index c8b266c0..00000000 Binary files a/share/shell32.png and /dev/null differ diff --git a/share/skewX.png b/share/skewX.png deleted file mode 100644 index 93f726ed..00000000 Binary files a/share/skewX.png and /dev/null differ diff --git a/share/skewY.png b/share/skewY.png deleted file mode 100644 index a56410de..00000000 Binary files a/share/skewY.png and /dev/null differ diff --git a/share/subtract16.png b/share/subtract16.png deleted file mode 100644 index 10673446..00000000 Binary files a/share/subtract16.png and /dev/null differ diff --git a/share/subtract24.png b/share/subtract24.png deleted file mode 100644 index 209cd986..00000000 Binary files a/share/subtract24.png and /dev/null differ diff --git a/share/subtract32.png b/share/subtract32.png deleted file mode 100644 index a4d0415e..00000000 Binary files a/share/subtract32.png and /dev/null differ diff --git a/share/transfer.png b/share/transfer.png deleted file mode 100644 index 2ba13e89..00000000 Binary files a/share/transfer.png and /dev/null differ diff --git a/share/transform.png b/share/transform.png deleted file mode 100644 index 6463d378..00000000 Binary files a/share/transform.png and /dev/null differ diff --git a/share/trash16.png b/share/trash16.png deleted file mode 100644 index 9b9247d7..00000000 Binary files a/share/trash16.png and /dev/null differ diff --git a/share/tv16.png b/share/tv16.png deleted file mode 100644 index 3f2c3b72..00000000 Binary files a/share/tv16.png and /dev/null differ diff --git a/share/union16.png b/share/union16.png deleted file mode 100644 index 63262538..00000000 Binary files a/share/union16.png and /dev/null differ diff --git a/share/union32.png b/share/union32.png deleted file mode 100644 index c31863cc..00000000 Binary files a/share/union32.png and /dev/null differ diff --git a/share/warning.png b/share/warning.png deleted file mode 100644 index 10f094a7..00000000 Binary files a/share/warning.png and /dev/null differ diff --git a/share/yellowlight12.png b/share/yellowlight12.png deleted file mode 100644 index f1b20691..00000000 Binary files a/share/yellowlight12.png and /dev/null differ diff --git a/share/zoom_fit32.png b/share/zoom_fit32.png deleted file mode 100644 index 1b455f61..00000000 Binary files a/share/zoom_fit32.png and /dev/null differ diff --git a/share/zoom_in32.png b/share/zoom_in32.png deleted file mode 100644 index c17a4c3e..00000000 Binary files a/share/zoom_in32.png and /dev/null differ diff --git a/share/zoom_out32.png b/share/zoom_out32.png deleted file mode 100644 index edb9f318..00000000 Binary files a/share/zoom_out32.png and /dev/null differ diff --git a/svgparse.py b/svgparse.py deleted file mode 100644 index 1f401267..00000000 --- a/svgparse.py +++ /dev/null @@ -1,526 +0,0 @@ -############################################################ -# 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, Polygon -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 Polygon(points).buffer(0) - # 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 Polygon(pts).buffer(0) - # 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 Polygon(pts).buffer(0) - # 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 Polygon(points).buffer(0) - # 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 deleted file mode 100644 index 45a0e6c6..00000000 --- a/tclCommands/TclCommand.py +++ /dev/null @@ -1,417 +0,0 @@ -import sys -import re -import FlatCAMApp -import abc -import collections -from PyQt4 import QtCore -from contextlib import contextmanager -from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMObj - - -class TclCommand(object): - - # FlatCAMApp - app = None - - # Logger - log = None - - # List 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() - - # List 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 = [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 list(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): - """ - - :param help_key: Name of the argument. - :param help_text: - :param in_command: - :return: - """ - 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 = help_key + "<" + type_name + ">" - in_command_name = help_key - - 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 list(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 = list(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 as 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 as 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, unknown_exception): - """ - raise Exception if is different type than TclErrorException - this is here mainly to show unknown errors inside TCL shell console - - :param unknown_exception: - :return: - """ - - raise unknown_exception - - 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(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 - """ - - @abc.abstractmethod - def execute(self, args, unnamed_args): - raise NotImplementedError("Please Implement this method") - - 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) diff --git a/tclCommands/TclCommandAddCircle.py b/tclCommands/TclCommandAddCircle.py deleted file mode 100644 index 868848a4..00000000 --- a/tclCommands/TclCommandAddCircle.py +++ /dev/null @@ -1,64 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAddCircle(TclCommand): - """ - Tcl shell command to creates a circle in the given Geometry object. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['add_circle'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('center_x', float), - ('center_y', float), - ('radius', float) - ]) - - # 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', 'center_x', 'center_y', 'radius'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Creates a circle in the given Geometry object.", - 'args': collections.OrderedDict([ - ('name', 'Name of the geometry object to which to append the circle.'), - ('center_x', 'X coordinate of the center of the circle.'), - ('center_y', 'Y coordinates of the center of the circle.'), - ('radius', 'Radius of the circle.') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - obj_name = args['name'] - center_x = args['center_x'] - center_y = args['center_y'] - radius = args['radius'] - - try: - obj = self.app.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.add_circle([float(center_x), float(center_y)], float(radius)) - diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py deleted file mode 100644 index 79fd5b02..00000000 --- a/tclCommands/TclCommandAddPolygon.py +++ /dev/null @@ -1,60 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAddPolygon(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 deleted file mode 100644 index f449e0c2..00000000 --- a/tclCommands/TclCommandAddPolyline.py +++ /dev/null @@ -1,60 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAddPolyline(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/TclCommandAddRectangle.py b/tclCommands/TclCommandAddRectangle.py deleted file mode 100644 index e190a399..00000000 --- a/tclCommands/TclCommandAddRectangle.py +++ /dev/null @@ -1,65 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAddRectangle(TclCommandSignaled): - """ - Tcl shell command to add a rectange to the given Geometry object. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['add_rectangle'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str), - ('x0', float), - ('y0', float), - ('x1', float), - ('y1', float) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'x0', 'y0', 'x1', 'y1'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Add a rectange to the given Geometry object.", - 'args': collections.OrderedDict([ - ('name', 'Name of the Geometry object in which to add the rectangle.'), - ('x0 y0', 'Bottom left corner coordinates.'), - ('x1 y1', 'Top right corner coordinates.') - ]), - '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 - """ - - obj_name = args['name'] - x0 = args['x0'] - y0 = args['y0'] - x1 = args['x1'] - y1 = args['y1'] - - try: - obj = self.app.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.add_polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) diff --git a/tclCommands/TclCommandAlignDrill.py b/tclCommands/TclCommandAlignDrill.py deleted file mode 100644 index 1cd1f129..00000000 --- a/tclCommands/TclCommandAlignDrill.py +++ /dev/null @@ -1,200 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAlignDrill(TclCommandSignaled): - """ - Tcl shell command to create excellon with drills for aligment. - """ - - # array of all command aliases, to be able use old names for - # backward compatibility (add_poly, add_polygon) - aliases = ['aligndrill'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('box', str), - ('axis', str), - ('holes', str), - ('grid', float), - ('minoffset', float), - ('gridoffset', float), - ('axisoffset', float), - ('dia', float), - ('dist', float), - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'axis'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Create excellon with drills for aligment.", - 'args': collections.OrderedDict([ - ('name', 'Name of the object (Gerber or Excellon) to mirror.'), - ('dia', 'Tool diameter'), - ('box', 'Name of object which act as box (cutout for example.)'), - ('grid', 'Aligning to grid, for those, 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.'), - ('dist', 'Distance of the mirror axis to the X or Y axis.') - ]), - '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'] - - # Get source object. - try: - obj = self.app.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 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: - axis = args['axis'].upper() - except KeyError: - return "ERROR: Specify -axis X or -axis Y" - - if not ('holes' in args or ('grid' in args and 'gridoffset' in args)): - return "ERROR: Specify -holes or -grid with -gridoffset " - - if 'holes' in args: - try: - holes = eval("[" + args['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": args['dia']}} - - def alligndrill_init_me(init_obj, app_obj): - """ - This function is used to initialize the new - object once it's created. - - :param init_obj: The new object. - :param app_obj: The application (FlatCAMApp) - :return: None - """ - - drills = [] - if 'holes' in args: - 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 'box' not in args: - return "ERROR: -grid can be used only for -box" - - if 'axisoffset' in args: - axisoffset = args['axisoffset'] - else: - axisoffset = 0 - - # This will align hole to given aligngridoffset and minimal offset from pcb, based on selected axis - if axis == "X": - firstpoint = args['gridoffset'] - - while (xmin - args['minoffset']) < firstpoint: - firstpoint = firstpoint - args['grid'] - - lastpoint = args['gridoffset'] - - while (xmax + args['minoffset']) > lastpoint: - lastpoint = lastpoint + args['grid'] - - localholes = (firstpoint, axisoffset), (lastpoint, axisoffset) - - else: - firstpoint = args['gridoffset'] - - while (ymin - args['minoffset']) < firstpoint: - firstpoint = firstpoint - args['grid'] - - lastpoint = args['gridoffset'] - - while (ymax + args['minoffset']) > lastpoint: - lastpoint = lastpoint + args['grid'] - - localholes = (axisoffset, firstpoint), (axisoffset, lastpoint) - - 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"}) - - init_obj.tools = tools - init_obj.drills = drills - init_obj.create_geometry() - - # Box - if 'box' in args: - try: - box = self.app.collection.get_by_name(args['box']) - except: - return "Could not retrieve object box: %s" % args['box'] - - if box is None: - return "Object box not found: %s" % args['box'] - - try: - xmin, ymin, xmax, ymax = box.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - obj.app.new_object("excellon", - name + "_aligndrill", - alligndrill_init_me) - - except Exception as e: - return "Operation failed: %s" % str(e) - - else: - try: - dist = float(args['dist']) - except KeyError: - dist = 0.0 - except ValueError: - return "Invalid distance: %s" % args['dist'] - - try: - px = dist - py = dist - obj.app.new_object("excellon", name + "_alligndrill", alligndrill_init_me) - except Exception as e: - return "Operation failed: %s" % str(e) - - return 'Ok' diff --git a/tclCommands/TclCommandAlignDrillGrid.py b/tclCommands/TclCommandAlignDrillGrid.py deleted file mode 100644 index 2cac28a1..00000000 --- a/tclCommands/TclCommandAlignDrillGrid.py +++ /dev/null @@ -1,104 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandAlignDrillGrid(TclCommandSignaled): - """ - Tcl shell command to create an Excellon object - with drills for aligment grid. - - Todo: What is an alignment grid? - """ - - # array of all command aliases, to be able use old names for - # backward compatibility (add_poly, add_polygon) - aliases = ['aligndrillgrid'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('outname', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('dia', float), - ('gridx', float), - ('gridxoffset', float), - ('gridy', float), - ('gridyoffset', float), - ('columns', int), - ('rows', int) - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['outname', 'gridx', 'gridy', 'columns', 'rows'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Create excellon with drills for aligment grid.", - 'args': collections.OrderedDict([ - ('outname', 'Name of the object to create.'), - ('dia', 'Tool diameter.'), - ('gridx', 'Grid size in X axis.'), - ('gridoffsetx', 'Move grid from origin.'), - ('gridy', 'Grid size in Y axis.'), - ('gridoffsety', 'Move grid from origin.'), - ('colums', 'Number of grid holes on X axis.'), - ('rows', 'Number of grid holes on Y axis.'), - ]), - '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 - """ - - if 'gridoffsetx' not in args: - gridoffsetx = 0 - else: - gridoffsetx = args['gridoffsetx'] - - if 'gridoffsety' not in args: - gridoffsety = 0 - else: - gridoffsety = args['gridoffsety'] - - # Tools - tools = {"1": {"C": args['dia']}} - - def aligndrillgrid_init_me(init_obj, app_obj): - """ - This function is used to initialize the new - object once it's created. - - :param init_obj: The new object. - :param app_obj: The application (FlatCAMApp) - :return: None - """ - - drills = [] - currenty = 0 - - for row in range(args['rows']): - currentx = 0 - - for col in range(args['columns']): - point = Point(currentx + gridoffsetx, currenty + gridoffsety) - drills.append({"point": point, "tool": "1"}) - currentx = currentx + args['gridx'] - - currenty = currenty + args['gridy'] - - init_obj.tools = tools - init_obj.drills = drills - init_obj.create_geometry() - - # Create the new object - self.new_object("excellon", args['outname'], aligndrillgrid_init_me) diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py deleted file mode 100644 index 09a1260a..00000000 --- a/tclCommands/TclCommandCncjob.py +++ /dev/null @@ -1,79 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandCncjob(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) diff --git a/tclCommands/TclCommandCutout.py b/tclCommands/TclCommandCutout.py deleted file mode 100644 index 8d0cc4f8..00000000 --- a/tclCommands/TclCommandCutout.py +++ /dev/null @@ -1,98 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandCutout(TclCommand): - """ - Tcl shell command to create a board cutout geometry. - - example: - - """ - - # List of all command aliases, to be able use old - # names for backward compatibility (add_poly, add_polygon) - aliases = ['cutout'] - - # 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), - ('margin', float), - ('gapsize', float), - ('gaps', 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 board cutout.', - 'args': collections.OrderedDict([ - ('name', 'Name of the object.'), - ('dia', 'Tool diameter.'), - ('margin', 'Margin over bounds.'), - ('gapsize', 'size of gap.'), - ('gaps', 'type of gaps.'), - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - - try: - obj = self.app.collection.get_by_name(str(name)) - except: - return "Could not retrieve object: %s" % name - - def geo_init_me(geo_obj, app_obj): - margin = args['margin'] + args['dia'] / 2 - gap_size = args['dia'] + args['gapsize'] - minx, miny, maxx, maxy = obj.bounds() - minx -= margin - maxx += margin - miny -= margin - maxy += margin - midx = 0.5 * (minx + maxx) - midy = 0.5 * (miny + maxy) - hgap = 0.5 * gap_size - pts = [[midx - hgap, maxy], - [minx, maxy], - [minx, midy + hgap], - [minx, midy - hgap], - [minx, miny], - [midx - hgap, miny], - [midx + hgap, miny], - [maxx, miny], - [maxx, midy - hgap], - [maxx, midy + hgap], - [maxx, maxy], - [midx + hgap, maxy]] - cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]], - [pts[6], pts[7], pts[10], pts[11]]], - "lr": [[pts[9], pts[10], pts[1], pts[2]], - [pts[3], pts[4], pts[7], pts[8]]], - "4": [[pts[0], pts[1], pts[2]], - [pts[3], pts[4], pts[5]], - [pts[6], pts[7], pts[8]], - [pts[9], pts[10], pts[11]]]} - cuts = cases[args['gaps']] - geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) - - try: - obj.app.new_object("geometry", name + "_cutout", geo_init_me) - except Exception as e: - return "Operation failed: %s" % str(e) diff --git a/tclCommands/TclCommandDelete.py b/tclCommands/TclCommandDelete.py deleted file mode 100644 index 8706722b..00000000 --- a/tclCommands/TclCommandDelete.py +++ /dev/null @@ -1,53 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandDelete(TclCommand): - """ - Tcl shell command to delete an object. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['delete'] - - # 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': 'Deletes the given object.', - 'args': collections.OrderedDict([ - ('name', 'Name of the Object.'), - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - obj_name = args['name'] - - try: - # deselect all to avoid delete selected object when run delete from shell - self.app.collection.set_all_inactive() - self.app.collection.set_active(str(obj_name)) - self.app.on_delete() # Todo: This is an event handler for the GUI... bad? - except Exception as e: - return "Command failed: %s" % str(e) diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py deleted file mode 100644 index cba89704..00000000 --- a/tclCommands/TclCommandDrillcncjob.py +++ /dev/null @@ -1,84 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandDrillcncjob(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"] if "drillz" in args else obj.options["drillz"] - job_obj.z_move = args["travelz"] if "travelz" in args else obj.options["travelz"] - job_obj.feedrate = args["feedrate"] if "feedrate" in args else obj.options["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 deleted file mode 100644 index d5f10df2..00000000 --- a/tclCommands/TclCommandExportGcode.py +++ /dev/null @@ -1,78 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandExportGcode(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 captured and passed as preable - to another "export_gcode" or "write_gcode" call to join G-Code. - - 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/TclCommandExportSVG.py b/tclCommands/TclCommandExportSVG.py deleted file mode 100644 index 07fb65b6..00000000 --- a/tclCommands/TclCommandExportSVG.py +++ /dev/null @@ -1,52 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandExportSVG(TclCommand): - """ - Tcl shell command to export a Geometry Object as an SVG File. - - example: - export_svg my_geometry filename - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['export_svg'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('filename', str), - ('scale_factor', float) - ]) - - # 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', 'filename'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Export a Geometry Object as a SVG File.", - 'args': collections.OrderedDict([ - ('name', 'Name of the object export.'), - ('filename', 'Path to the file to export.'), - ('scale_factor', 'Multiplication factor used for scaling line widths during export.') - ]), - 'examples': ['export_svg my_geometry my_file.svg'] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - filename = args['filename'] - - self.app.export_svg(name, filename, **args) diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py deleted file mode 100644 index 750ec6ce..00000000 --- a/tclCommands/TclCommandExteriors.py +++ /dev/null @@ -1,63 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandExteriors(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, FlatCAMGeometry): - 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/TclCommandFollow.py b/tclCommands/TclCommandFollow.py deleted file mode 100644 index 36ec3f31..00000000 --- a/tclCommands/TclCommandFollow.py +++ /dev/null @@ -1,61 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandFollow(TclCommandSignaled): - """ - Tcl shell command to follow a Gerber file - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['follow'] - - # 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': "Creates a geometry object following gerber paths.", - 'args': collections.OrderedDict([ - ('name', 'Object name to follow.'), - ('outname', 'Name of the resulting Geometry object.') - ]), - 'examples': ['follow name -outname name_follow'] - } - - 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: - follow_name = name + "_follow" - - 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.follow(**args) - - diff --git a/tclCommands/TclCommandGeoCutout.py b/tclCommands/TclCommandGeoCutout.py deleted file mode 100644 index bca56b72..00000000 --- a/tclCommands/TclCommandGeoCutout.py +++ /dev/null @@ -1,117 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandGeoCutout(TclCommandSignaled): - """ - Tcl shell command to cut holding gaps from geometry. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['geocutout'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('dia', float), - ('margin', float), - ('gapsize', float), - ('gaps', 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': "Cut holding gaps from geometry.", - 'args': collections.OrderedDict([ - ('name', 'Name of the geometry object.'), - ('dia', 'Tool diameter.'), - ('margin', 'Margin over bounds.'), - ('gapsize', 'Size of gap.'), - ('gaps', 'Type of gaps.'), - ]), - '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 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 - - name = args['name'] - obj = None - - def subtract_rectangle(obj_, x0, y0, x1, y1): - pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] - obj_.subtract_polygon(pts) - - try: - obj = self.app.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 = args['gapsize'] + args['dia'] / 2 - - if args['gaps'] == '8' or args['gaps'] == '2lr': - subtract_rectangle(obj, - xmin - gapsize, # botleft_x - py - gapsize + lenghty / 4, # botleft_y - xmax + gapsize, # topright_x - py + gapsize + lenghty / 4) # topright_y - subtract_rectangle(obj, - xmin - gapsize, - py - gapsize - lenghty / 4, - xmax + gapsize, - py + gapsize - lenghty / 4) - - if args['gaps'] == '8' or args['gaps'] == '2tb': - subtract_rectangle(obj, - px - gapsize + lenghtx / 4, - ymin - gapsize, - px + gapsize + lenghtx / 4, - ymax + gapsize) - subtract_rectangle(obj, - px - gapsize - lenghtx / 4, - ymin - gapsize, - px + gapsize - lenghtx / 4, - ymax + gapsize) - - if args['gaps'] == '4' or args['gaps'] == 'lr': - subtract_rectangle(obj, - xmin - gapsize, - py - gapsize, - xmax + gapsize, - py + gapsize) - - if args['gaps'] == '4' or args['gaps'] == 'tb': - subtract_rectangle(obj, - px - gapsize, - ymin - gapsize, - px + gapsize, - ymax + gapsize) diff --git a/tclCommands/TclCommandGeoUnion.py b/tclCommands/TclCommandGeoUnion.py deleted file mode 100644 index 3ecbb6d4..00000000 --- a/tclCommands/TclCommandGeoUnion.py +++ /dev/null @@ -1,58 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandGeoUnion(TclCommand): - """ - Tcl shell command to run a union (addition) operation on the - components of a geometry object. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['geo_union'] - - # 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': ('Runs a union operation (addition) on the components ' - 'of the geometry object. For example, if it contains ' - '2 intersecting polygons, this opperation adds them into' - 'a single larger polygon.'), - 'args': collections.OrderedDict([ - ('name', 'Name of the Geometry Object.'), - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - obj_name = args['name'] - - 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.union() diff --git a/tclCommands/TclCommandGetNames.py b/tclCommands/TclCommandGetNames.py deleted file mode 100644 index e6e0a461..00000000 --- a/tclCommands/TclCommandGetNames.py +++ /dev/null @@ -1,45 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandGetNames(TclCommand): - """ - Tcl shell command to set an object as active in the GUI. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['get_names'] - - # 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': 'Lists the names of objects in the project.', - 'args': collections.OrderedDict([ - - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - return '\n'.join(self.app.collection.get_names()) diff --git a/tclCommands/TclCommandGetSys.py b/tclCommands/TclCommandGetSys.py deleted file mode 100644 index 2cb213c4..00000000 --- a/tclCommands/TclCommandGetSys.py +++ /dev/null @@ -1,49 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandGetSys(TclCommand): - """ - Tcl shell command to get the value of a system variable - - example: - get_sys excellon_zeros - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['get_sys', 'getsys'] - - # 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': "Returns the value of the system variable.", - 'args': collections.OrderedDict([ - ('name', 'Name of the system variable.'), - ]), - 'examples': ['get_sys excellon_zeros'] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - - if name in self.app.defaults: - return self.app.defaults[name] - diff --git a/tclCommands/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py deleted file mode 100644 index b49dac02..00000000 --- a/tclCommands/TclCommandImportSvg.py +++ /dev/null @@ -1,80 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandImportSvg(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 deleted file mode 100644 index 70d37c48..00000000 --- a/tclCommands/TclCommandInteriors.py +++ /dev/null @@ -1,63 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandInteriors(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 deleted file mode 100644 index dc1ab1fa..00000000 --- a/tclCommands/TclCommandIsolate.py +++ /dev/null @@ -1,78 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandIsolate(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/TclCommandJoinExcellon.py b/tclCommands/TclCommandJoinExcellon.py deleted file mode 100644 index 64c20e78..00000000 --- a/tclCommands/TclCommandJoinExcellon.py +++ /dev/null @@ -1,63 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandJoinExcellon(TclCommand): - """ - Tcl shell command to merge Excellon objects. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['join_excellon', 'join_excellons'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('outname', 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 = ['outname'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Runs a merge operation (join) on the Excellon objects.", - 'args': collections.OrderedDict([ - ('name', 'Name of the new Excellon Object.'), - ('obj_name_0', 'Name of the first object'), - ('obj_name_1', 'Name of the second object.'), - ('obj_name_2...', 'Additional object names') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - outname = args['name'] - obj_names = unnamed_args - - objs = [] - for obj_n in obj_names: - obj = self.app.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.app.new_object("excellon", outname, initialize) diff --git a/tclCommands/TclCommandJoinGeometry.py b/tclCommands/TclCommandJoinGeometry.py deleted file mode 100644 index 18ca2d38..00000000 --- a/tclCommands/TclCommandJoinGeometry.py +++ /dev/null @@ -1,63 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandJoinGeometry(TclCommand): - """ - Tcl shell command to merge Excellon objects. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['join_geometries', 'join_geometry'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('outname', 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 = ['outname'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Runs a merge operation (join) on the Excellon objects.", - 'args': collections.OrderedDict([ - ('outname', 'Name of the new Geometry Object.'), - ('obj_name_0', 'Name of the first object'), - ('obj_name_1', 'Name of the second object.'), - ('obj_name_2...', 'Additional object names') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - outname = args['outname'] - obj_names = unnamed_args - - objs = [] - for obj_n in obj_names: - obj = self.app.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): - FlatCAMGeometry.merge(objs, obj_) - - if objs is not None: - self.app.new_object("geometry", outname, initialize) diff --git a/tclCommands/TclCommandListSys.py b/tclCommands/TclCommandListSys.py deleted file mode 100644 index 87d5a761..00000000 --- a/tclCommands/TclCommandListSys.py +++ /dev/null @@ -1,56 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandListSys(TclCommand): - """ - Tcl shell command to get the list of system variables - - example: - list_sys - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['list_sys', 'listsys'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('selection', 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 = [] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Returns the list of the names of system variables.\n" - "Without an argument it will list all the system parameters. " - "As an argument use first letter or first letters from the name " - "of the system variable.\n" - "In that case it will list only the system variables that starts with that string.\n" - "Main categories start with: gerber or excellon or geometry or cncjob or global.\n" - "Note: Use get_sys TclCommand to get the value and set_sys TclCommand to set it.\n", - 'args': collections.OrderedDict([ - ]), - 'examples': ['list_sys', - 'list_sys ser' - 'list_sys gerber', - 'list_sys cncj'] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - if 'selection' in args: - argument = args['selection'] - return str([k for k in self.app.defaults.keys() if str(k).startswith(str(argument))]) - else: - return str([*self.app.defaults]) \ No newline at end of file diff --git a/tclCommands/TclCommandMillHoles.py b/tclCommands/TclCommandMillHoles.py deleted file mode 100644 index 214b4226..00000000 --- a/tclCommands/TclCommandMillHoles.py +++ /dev/null @@ -1,85 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandMillHoles(TclCommandSignaled): - """ - Tcl shell command to Create Geometry Object for milling holes from Excellon. - - example: - millholes my_drill -tools 1,2,3 -tooldia 0.1 -outname mill_holes_geo - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['millholes'] - - # 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), - ('outname', str), - ('tooldia', float) - ]) - - # 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': "Create Geometry Object for milling holes from Excellon.", - 'args': collections.OrderedDict([ - ('name', 'Name of the Excellon Object.'), - ('tools', 'Comma separated indexes of tools (example: 1,3 or 2).'), - ('tooldia', 'Diameter of the milling tool (example: 0.1).'), - ('outname', 'Name of object to create.') - ]), - 'examples': ['millholes mydrills'] - } - - def execute(self, args, unnamed_args): - """ - - :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 + "_mill" - - try: - if 'tools' in args and args['tools'] != 'all': - # Split and put back. We are passing the whole dictionary later. - args['tools'] = [x.strip() for x in args['tools'].split(",")] - else: - args['tools'] = 'all' - except Exception as e: - self.raise_tcl_error("Bad tools: %s" % str(e)) - - try: - obj = self.app.collection.get_by_name(str(name)) - except: - self.raise_tcl_error("Could not retrieve object: %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: - # 'name' is not an argument of obj.generate_milling() - del args['name'] - - # This runs in the background... Is blocking handled? - success, msg = obj.generate_milling(**args) - - except Exception as e: - self.raise_tcl_error("Operation failed: %s" % str(e)) - - if not success: - self.raise_tcl_error(msg) diff --git a/tclCommands/TclCommandMirror.py b/tclCommands/TclCommandMirror.py deleted file mode 100644 index 437d741b..00000000 --- a/tclCommands/TclCommandMirror.py +++ /dev/null @@ -1,107 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandMirror(TclCommandSignaled): - """ - Tcl shell command to mirror an object. - """ - - # array of all command aliases, to be able use - # old names for backward compatibility (add_poly, add_polygon) - aliases = ['mirror'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('axis', str), - ('box', str), - ('dist', float) - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'axis'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Opens an Excellon file.", - 'args': collections.OrderedDict([ - ('name', 'Name of the object (Gerber or Excellon) to mirror.'), - ('box', 'Name of object which act as box (cutout for example.)'), - ('axis', 'Mirror axis parallel to the X or Y axis.'), - ('dist', 'Distance of the mirror axis to the X or Y axis.') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - Execute this 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'] - - # Get source object. - try: - obj = self.app.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 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: - axis = args['axis'].upper() - except KeyError: - return "ERROR: Specify -axis X or -axis Y" - - # Box - if 'box' in args: - try: - box = self.app.collection.get_by_name(args['box']) - except: - return "Could not retrieve object box: %s" % args['box'] - - if box is None: - return "Object box not found: %s" % args['box'] - - try: - xmin, ymin, xmax, ymax = box.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - obj.mirror(axis, [px, py]) - obj.plot() - - except Exception as e: - return "Operation failed: %s" % str(e) - - else: - try: - dist = float(args['dist']) - except KeyError: - dist = 0.0 - except ValueError: - return "Invalid distance: %s" % args['dist'] - - try: - obj.mirror(axis, [dist, dist]) - obj.plot() - except Exception as e: - return "Operation failed: %s" % str(e) diff --git a/tclCommands/TclCommandNew.py b/tclCommands/TclCommandNew.py deleted file mode 100644 index 2607035a..00000000 --- a/tclCommands/TclCommandNew.py +++ /dev/null @@ -1,38 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandNew(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/TclCommandNewGeometry.py b/tclCommands/TclCommandNewGeometry.py deleted file mode 100644 index b543c439..00000000 --- a/tclCommands/TclCommandNewGeometry.py +++ /dev/null @@ -1,48 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandNewGeometry(TclCommandSignaled): - """ - Tcl shell command to subtract polygon from the given Geometry object. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['new_geometry'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # 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 new empty geometry object.", - 'args': collections.OrderedDict([ - ('name', 'New object name.'), - ]), - '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'] - - self.app.new_object('geometry', str(name), lambda x, y: None) diff --git a/tclCommands/TclCommandOffset.py b/tclCommands/TclCommandOffset.py deleted file mode 100644 index 09f5f28e..00000000 --- a/tclCommands/TclCommandOffset.py +++ /dev/null @@ -1,52 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOffset(TclCommand): - """ - Tcl shell command to change the position of the object. - - example: - offset my_geometry 1.2 -0.3 - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['offset'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('x', float), - ('y', float) - ]) - - # 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', 'x', 'y'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Changes the position of the object.", - 'args': collections.OrderedDict([ - ('name', 'Name of the object to offset.'), - ('x', 'Offset distance in the X axis.'), - ('y', 'Offset distance in the Y axis') - ]), - 'examples': ['offset my_geometry 1.2 -0.3'] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - x, y = args['x'], args['y'] - - self.app.collection.get_by_name(name).offset((x, y)) diff --git a/tclCommands/TclCommandOpenExcellon.py b/tclCommands/TclCommandOpenExcellon.py deleted file mode 100644 index 3b731fa6..00000000 --- a/tclCommands/TclCommandOpenExcellon.py +++ /dev/null @@ -1,49 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOpenExcellon(TclCommandSignaled): - """ - Tcl shell command to open an Excellon file. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['open_excellon'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('filename', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('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 an Excellon file.", - 'args': collections.OrderedDict([ - ('filename', 'Path to file to open.'), - ('outname', 'Name of the resulting Excellon 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 - """ - - filename = args.pop('filename') - - self.app.open_excellon(filename, **args) diff --git a/tclCommands/TclCommandOpenGCode.py b/tclCommands/TclCommandOpenGCode.py deleted file mode 100644 index 10c475b4..00000000 --- a/tclCommands/TclCommandOpenGCode.py +++ /dev/null @@ -1,48 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOpenGCode(TclCommandSignaled): - """ - Tcl shell command to open a G-Code file. - """ - - # array of all command aliases, to be able use old names for - # backward compatibility (add_poly, add_polygon) - aliases = ['open_gcode'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('filename', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('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 G-Code file.", - 'args': collections.OrderedDict([ - ('filename', 'Path to file to open.'), - ('outname', 'Name of the resulting CNCJob 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 - """ - - self.app.open_gcode(args['filename'], **args) diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py deleted file mode 100644 index 0ad4bba7..00000000 --- a/tclCommands/TclCommandOpenGerber.py +++ /dev/null @@ -1,94 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOpenGerber(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 Gerber 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, FlatCAMGerber): - 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 as 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/TclCommandOpenProject.py b/tclCommands/TclCommandOpenProject.py deleted file mode 100644 index 971f4f96..00000000 --- a/tclCommands/TclCommandOpenProject.py +++ /dev/null @@ -1,46 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOpenProject(TclCommandSignaled): - """ - Tcl shell command to open a FlatCAM project. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['open_project'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('filename', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - - ]) - - # 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 FlatCAM project.", - 'args': collections.OrderedDict([ - ('filename', 'Path to file to open.'), - ]), - '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.open_project(args['filename']) diff --git a/tclCommands/TclCommandOptions.py b/tclCommands/TclCommandOptions.py deleted file mode 100644 index 7f185621..00000000 --- a/tclCommands/TclCommandOptions.py +++ /dev/null @@ -1,49 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandOptions(TclCommandSignaled): - """ - Tcl shell command to open an Excellon file. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['options'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # 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': "Shows the settings for an object.", - 'args': collections.OrderedDict([ - ('name', 'Object name.'), - ]), - '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'] - - ops = self.app.collection.get_by_name(str(name)).options - return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops]) diff --git a/tclCommands/TclCommandPaint.py b/tclCommands/TclCommandPaint.py deleted file mode 100644 index f3222adb..00000000 --- a/tclCommands/TclCommandPaint.py +++ /dev/null @@ -1,83 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandPaint(TclCommandSignaled): - """ - Paint the interior of polygons - """ - - # Array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['paint'] - - # dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('tooldia', float), - ('overlap', float) - ]) - - # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([ - ('outname', str), - ('all', bool), - ('x', float), - ('y', float) - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'tooldia', 'overlap'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Paint polygons", - 'args': collections.OrderedDict([ - ('name', 'Name of the source Geometry object.'), - ('tooldia', 'Diameter of the tool to be used.'), - ('overlap', 'Fraction of the tool diameter to overlap cuts.'), - ('outname', 'Name of the resulting Geometry object.'), - ('all', 'Paint all polygons in the object.'), - ('x', 'X value of coordinate for the selection of a single polygon.'), - ('y', 'Y value of coordinate for the selection of a single polygon.') - ]), - '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'] - tooldia = args['tooldia'] - overlap = args['overlap'] - - if 'outname' in args: - outname = args['outname'] - else: - outname = name + "_paint" - - 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 'all' in args and args['all']: - obj.paint_poly_all(tooldia, overlap, outname) - return - - if 'x' not in args or 'y' not in args: - self.raise_tcl_error('Expected -all 1 or -x and -y .') - - x = args['x'] - y = args['y'] - - obj.paint_poly_single_click([x, y], tooldia, overlap, outname) - - diff --git a/tclCommands/TclCommandPanelize.py b/tclCommands/TclCommandPanelize.py deleted file mode 100644 index 02e7f57c..00000000 --- a/tclCommands/TclCommandPanelize.py +++ /dev/null @@ -1,151 +0,0 @@ -from copy import copy,deepcopy -from tclCommands.TclCommand import * - - -class TclCommandPanelize(TclCommand): - """ - Tcl shell command to pannelize an object. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['panelize', 'pan', 'panel'] - - # 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([ - ('rows', int), - ('columns', int), - ('spacing_columns', float), - ('spacing_rows', float), - ('box', str), - ('outname', str) - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'rows', 'columns'] - - # structured help for current command, args needs to be ordered - help = { - 'main': 'Rectangular panelizing.', - 'args': collections.OrderedDict([ - ('name', 'Name of the object to panelize.'), - ('box', 'Name of object which acts as box (cutout for example.)' - 'for cutout boundary. Object from name is used if not specified.'), - ('spacing_columns', 'Spacing between columns.'), - ('spacing_rows', 'Spacing between rows.'), - ('columns', 'Number of columns.'), - ('rows', 'Number of rows;'), - ('outname', 'Name of the new geometry object.') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - - # Get source object. - try: - obj = self.app.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 args: - boxname = args['box'] - try: - box = self.app.collection.get_by_name(boxname) - except: - return "Could not retrieve object: %s" % name - else: - box = obj - - if 'columns' not in args or 'rows' not in args: - return "ERROR: Specify -columns and -rows" - - if 'outname' in args: - outname = args['outname'] - else: - outname = name + '_panelized' - - if 'spacing_columns' in args: - spacing_columns = args['spacing_columns'] - else: - spacing_columns = 5 - - if 'spacing_rows' in args: - spacing_rows = args['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)]), - objs.append(obj_init) - - def initialize_local_excellon(obj_init, app): - obj_init.tools = obj.tools - # drills are offset, so they need to be deep copied - obj_init.drills = deepcopy(obj.drills) - obj_init.offset([float(currentx), float(currenty)]) - obj_init.create_geometry() - objs.append(obj_init) - - def initialize_geometry(obj_init, app): - FlatCAMGeometry.merge(objs, obj_init) - - def initialize_excellon(obj_init, app): - # merge expects tools to exist in the target object - obj_init.tools = obj.tools.copy() - FlatCAMExcellon.merge(objs, obj_init) - - objs = [] - if obj is not None: - - for row in range(args['rows']): - currentx = 0 - for col in range(args['columns']): - local_outname = outname + ".tmp." + str(col) + "." + str(row) - if isinstance(obj, FlatCAMExcellon): - self.app.new_object("excellon", local_outname, initialize_local_excellon, plot=False) - else: - self.app.new_object("geometry", local_outname, initialize_local, plot=False) - currentx += lenghtx - currenty += lenghty - - if isinstance(obj, FlatCAMExcellon): - self.app.new_object("excellon", outname, initialize_excellon) - else: - self.app.new_object("geometry", outname, initialize_geometry) - - # deselect all to avoid delete selected object when run delete from shell - self.app.collection.set_all_inactive() - for delobj in objs: - self.app.collection.set_active(delobj.options['name']) - self.app.on_delete() - - else: - return "ERROR: obj is None" - - return "Ok" diff --git a/tclCommands/TclCommandPlot.py b/tclCommands/TclCommandPlot.py deleted file mode 100644 index 687b34db..00000000 --- a/tclCommands/TclCommandPlot.py +++ /dev/null @@ -1,45 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandPlot(TclCommand): - """ - Tcl shell command to update the plot on the user interface. - - example: - plot - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['plot'] - - # 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': "Updates the plot on the user interface.", - 'args': collections.OrderedDict([ - - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - self.app.plot_all() diff --git a/tclCommands/TclCommandSaveProject.py b/tclCommands/TclCommandSaveProject.py deleted file mode 100644 index c4a44c5d..00000000 --- a/tclCommands/TclCommandSaveProject.py +++ /dev/null @@ -1,46 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandSaveProject(TclCommandSignaled): - """ - Tcl shell command to save the FlatCAM project to file. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['save_project'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('filename', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - - ]) - - # 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': "Saves the FlatCAM project to file.", - 'args': collections.OrderedDict([ - ('filename', 'Path to file.'), - ]), - '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.save_project(args['filename']) diff --git a/tclCommands/TclCommandScale.py b/tclCommands/TclCommandScale.py deleted file mode 100644 index 3b561992..00000000 --- a/tclCommands/TclCommandScale.py +++ /dev/null @@ -1,50 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandScale(TclCommand): - """ - Tcl shell command to resizes the object by a factor. - - example: - scale my_geometry 4.2 - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['scale'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('factor', float) - ]) - - # 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', 'factor'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Resizes the object by a factor.", - 'args': collections.OrderedDict([ - ('name', 'Name of the object to resize.'), - ('factor', 'Fraction by which to scale.') - ]), - 'examples': ['scale my_geometry 4.2'] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - name = args['name'] - factor = args['factor'] - - self.app.collection.get_by_name(name).scale(factor) diff --git a/tclCommands/TclCommandSetActive.py b/tclCommands/TclCommandSetActive.py deleted file mode 100644 index 19d9f000..00000000 --- a/tclCommands/TclCommandSetActive.py +++ /dev/null @@ -1,50 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandSetActive(TclCommand): - """ - Tcl shell command to set an object as active in the GUI. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['set_active'] - - # 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': 'Sets an object as active.', - 'args': collections.OrderedDict([ - ('name', 'Name of the Object.'), - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - obj_name = args['name'] - - try: - self.app.collection.set_active(str(obj_name)) - except Exception as e: - return "Command failed: %s" % str(e) diff --git a/tclCommands/TclCommandSetSys.py b/tclCommands/TclCommandSetSys.py deleted file mode 100644 index aff86124..00000000 --- a/tclCommands/TclCommandSetSys.py +++ /dev/null @@ -1,73 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandSetSys(TclCommand): - """ - Tcl shell command to set the value of a system variable - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['set_sys', 'setsys'] - - # Dictionary of types from Tcl command, needs to be ordered - arg_names = collections.OrderedDict([ - ('name', str), - ('value', 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', 'value'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Sets the value of the system variable.", - 'args': collections.OrderedDict([ - ('name', 'Name of the system variable.'), - ('value', 'Value to set.') - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - param = args['name'] - value = args['value'] - - # TCL string to python keywords: - tcl2py = { - "None": None, - "none": None, - "false": False, - "False": False, - "true": True, - "True": True, - "mm": "MM", - "in": "IN" - } - - if param in self.app.defaults: - - try: - value = tcl2py[value] - except KeyError: - pass - - self.app.defaults[param] = value - self.app.propagate_defaults() - else: - self.raise_tcl_error("No such system parameter \"{}\".".format(param)) - diff --git a/tclCommands/TclCommandSubtractPoly.py b/tclCommands/TclCommandSubtractPoly.py deleted file mode 100644 index 427b85f7..00000000 --- a/tclCommands/TclCommandSubtractPoly.py +++ /dev/null @@ -1,61 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandSubtractPoly(TclCommandSignaled): - """ - Tcl shell command to create a new empty Geometry object. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['subtract_poly'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # 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': "Subtract polygon from the given Geometry object.", - 'args': collections.OrderedDict([ - ('name', 'Name of the Geometry object from which to subtract.'), - ('x0 y0 x1 y1 x2 y2 ...', 'Points defining the polygon.') - ]), - '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 - """ - - obj_name = args['name'] - - if len(unnamed_args) % 2 != 0: - return "Incomplete coordinate." - - points = [[float(unnamed_args[2 * i]), float(unnamed_args[2 * i + 1])] for i in range(len(unnamed_args) / 2)] - - try: - obj = self.app.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) diff --git a/tclCommands/TclCommandSubtractRectangle.py b/tclCommands/TclCommandSubtractRectangle.py deleted file mode 100644 index 73d91284..00000000 --- a/tclCommands/TclCommandSubtractRectangle.py +++ /dev/null @@ -1,65 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandSubtractRectangle(TclCommandSignaled): - """ - Tcl shell command to subtract a rectange from the given Geometry object. - """ - - # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['subtract_rectangle'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str), - ('x0', float), - ('y0', float), - ('x1', float), - ('y1', float) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'x0', 'y0', 'x1', 'y1'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Subtract rectange from the given Geometry object.", - 'args': collections.OrderedDict([ - ('name', 'Name of the Geometry object from which to subtract.'), - ('x0 y0', 'Bottom left corner coordinates.'), - ('x1 y1', 'Top right corner coordinates.') - ]), - '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 - """ - - obj_name = args['name'] - x0 = args['x0'] - y0 = args['y0'] - x1 = args['x1'] - y1 = args['y1'] - - try: - obj = self.app.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([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) diff --git a/tclCommands/TclCommandVersion.py b/tclCommands/TclCommandVersion.py deleted file mode 100644 index a2977289..00000000 --- a/tclCommands/TclCommandVersion.py +++ /dev/null @@ -1,46 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandVersion(TclCommand): - """ - Tcl shell command to check the program version. - - example: - - """ - - # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['version'] - - # 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': "Checks the program version.", - 'args': collections.OrderedDict([ - - ]), - 'examples': [] - } - - def execute(self, args, unnamed_args): - """ - - :param args: - :param unnamed_args: - :return: - """ - - self.app.version_check() - diff --git a/tclCommands/TclCommandWriteGCode.py b/tclCommands/TclCommandWriteGCode.py deleted file mode 100644 index 17919150..00000000 --- a/tclCommands/TclCommandWriteGCode.py +++ /dev/null @@ -1,89 +0,0 @@ -from tclCommands.TclCommand import * - - -class TclCommandWriteGCode(TclCommandSignaled): - """ - Tcl shell command to save the G-code of a CNC Job object to file. - """ - - # array of all command aliases, to be able use - # old names for backward compatibility (add_poly, add_polygon) - aliases = ['write_gcode'] - - # Dictionary of types from Tcl command, needs to be ordered. - # For positional arguments - arg_names = collections.OrderedDict([ - ('name', str), - ('filename', str) - ]) - - # Dictionary of types from Tcl command, needs to be ordered. - # For options like -optionname value - option_types = collections.OrderedDict([ - ('preamble', str), - ('postamble', str) - ]) - - # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'filename'] - - # structured help for current command, args needs to be ordered - help = { - 'main': "Saves G-code of a CNC Job object to file.", - 'args': collections.OrderedDict([ - ('name', 'Source CNC Job object.'), - ('filename', 'Output filename.'), - ('preamble', 'Text to append at the beginning.'), - ('postamble', 'Text to append at the end.') - ]), - '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 - """ - - """ - Requires obj_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. - """ - - obj_name = args['name'] - filename = args['filename'] - - preamble = args['preamble'] if 'preamble' in args else '' - postamble = args['postamble'] if 'postamble' in args else '' - - # TODO: This is not needed any more? All targets should be present. - # If there are promised objects, wait until all promises have been fulfilled. - # if self.collection.has_promises(): - # def write_gcode_on_object(new_object): - # self.log.debug("write_gcode_on_object(): Disconnecting %s" % write_gcode_on_object) - # self.new_object_available.disconnect(write_gcode_on_object) - # write_gcode(obj_name, filename, preamble, postamble) - # - # # Try again when a new object becomes available. - # self.log.debug("write_gcode(): Collection has promises. Queued for %s." % obj_name) - # self.log.debug("write_gcode(): Queued function: %s" % write_gcode_on_object) - # self.new_object_available.connect(write_gcode_on_object) - # - # return - - # self.log.debug("write_gcode(): No promises. Continuing for %s." % obj_name) - - try: - obj = self.app.collection.get_by_name(str(obj_name)) - except: - return "Could not retrieve object: %s" % obj_name - - try: - obj.export_gcode(str(filename), str(preamble), str(postamble)) - except Exception as e: - return "Operation failed: %s" % str(e) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py deleted file mode 100644 index bb646c84..00000000 --- a/tclCommands/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -import pkgutil -import sys - -# Todo: I think these imports are not needed. -# allowed command modules (please append them alphabetically ordered) -import tclCommands.TclCommandAddCircle -import tclCommands.TclCommandAddPolygon -import tclCommands.TclCommandAddPolyline -import tclCommands.TclCommandAddRectangle -import tclCommands.TclCommandAlignDrill -import tclCommands.TclCommandAlignDrillGrid -import tclCommands.TclCommandCncjob -import tclCommands.TclCommandCutout -import tclCommands.TclCommandDelete -import tclCommands.TclCommandDrillcncjob -import tclCommands.TclCommandExportGcode -import tclCommands.TclCommandExportSVG -import tclCommands.TclCommandExteriors -import tclCommands.TclCommandFollow -import tclCommands.TclCommandGeoCutout -import tclCommands.TclCommandGeoUnion -import tclCommands.TclCommandGetNames -import tclCommands.TclCommandGetSys -import tclCommands.TclCommandImportSvg -import tclCommands.TclCommandInteriors -import tclCommands.TclCommandIsolate -import tclCommands.TclCommandJoinExcellon -import tclCommands.TclCommandJoinGeometry -import tclCommands.TclCommandListSys -import tclCommands.TclCommandMillHoles -import tclCommands.TclCommandMirror -import tclCommands.TclCommandNew -import tclCommands.TclCommandNewGeometry -import tclCommands.TclCommandOffset -import tclCommands.TclCommandOpenExcellon -import tclCommands.TclCommandOpenGCode -import tclCommands.TclCommandOpenGerber -import tclCommands.TclCommandOpenProject -import tclCommands.TclCommandOptions -import tclCommands.TclCommandPaint -import tclCommands.TclCommandPanelize -import tclCommands.TclCommandPlot -import tclCommands.TclCommandSaveProject -import tclCommands.TclCommandScale -import tclCommands.TclCommandSetActive -import tclCommands.TclCommandSetSys -import tclCommands.TclCommandSubtractPoly -import tclCommands.TclCommandSubtractRectangle -import tclCommands.TclCommandVersion -import tclCommands.TclCommandWriteGCode - - -__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 registers 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: List of commands being updated - :return: None - """ - - tcl_modules = {k: v for k, v in list(sys.modules.items()) if k.startswith('tclCommands.TclCommand')} - - for key, mod in list(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 deleted file mode 100644 index ba61edd8..00000000 --- a/termwidget.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -Terminal emulator widget. -Shows intput and output text. Allows to enter commands. Supports history. -""" - -import html -from PyQt4.QtCore import pyqtSignal, Qt -from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ - QSizePolicy, QTextCursor, QTextEdit, \ - QVBoxLayout, QWidget - - -class _ExpandableTextEdit(QTextEdit): - """ - Class implements edit line, which expands themselves automatically - """ - - historyNext = pyqtSignal() - historyPrev = pyqtSignal() - - 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 - - def sizeHint(self): - """ - QWidget sizeHint impelemtation - """ - hint = QTextEdit.sizeHint(self) - hint.setHeight(self._fittedHeight) - return hint - - def _fit_to_document(self): - """ - Update widget height to fit all text - """ - documentsize = self.document().size().toSize() - self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height()) - self.setMaximumHeight(self._fittedHeight) - self.updateGeometry() - - def keyPressEvent(self, event): - """ - Catch keyboard events. Process Enter, Up, Down - """ - if event.matches(QKeySequence.InsertParagraphSeparator): - text = self.toPlainText() - if self._termWidget.is_command_complete(text): - self._termWidget.exec_current_command() - return - elif event.matches(QKeySequence.MoveToNextLine): - text = self.toPlainText() - 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() - cursor_pos = self.textCursor().position() - text_before_start = text[:cursor_pos] - # lineCount = len(textBeforeStart.splitlines()) - 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): - 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): - """ - Widget wich represents terminal. It only displays text and allows to enter text. - All highlevel logic should be implemented by client classes - - User pressed Enter. Client class should decide, if command must be executed or user may continue edit it - """ - - def __init__(self, *args): - QWidget.__init__(self, *args) - - 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._edit = _ExpandableTextEdit(self, self) - self._edit.historyNext.connect(self._on_history_next) - self._edit.historyPrev.connect(self._on_history_prev) - self.setFocusProxy(self._edit) - - layout = QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._browser) - layout.addWidget(self._edit) - - self._history = [''] # current empty line - self._historyIndex = 0 - - 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) - self._edit.setFocus() - - def _append_to_browser(self, style, text): - """ - Convert text to HTML for inserting it to browser - """ - assert style in ('in', 'out', 'err') - - text = html.escape(text) - text = text.replace('\n', '
') - - if style == 'in': - text = '%s' % text - elif style == 'err': - text = '%s' % text - else: - text = '%s' % text # without span
is ignored!!! - - scrollbar = self._browser.verticalScrollBar() - old_value = scrollbar.value() - scrollattheend = old_value == scrollbar.maximum() - - self._browser.moveCursor(QTextCursor.End) - self._browser.insertHtml(text) - - """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon - and stops moving. As quick fix of this problem, now we always scroll down when add new text. - To fix it correctly, srcoll to the bottom, if before intput has been resized, - scrollbar was in the bottom, and remove next lien - """ - scrollattheend = True - - if scrollattheend: - scrollbar.setValue(scrollbar.maximum()) - else: - scrollbar.setValue(old_value) - - def exec_current_command(self): - """ - Save current command in the history. Append it to the log. Clear edit line - Reimplement in the child classes to actually execute command - """ - text = str(self._edit.toPlainText()) - self._append_to_browser('in', '> ' + text + '\n') - - if len(self._history) < 2 or\ - self._history[-2] != text: # don't insert duplicating items - if text[-1] == '\n': - self._history.insert(-1, text[:-1]) - else: - self._history.insert(-1, text) - - self._historyIndex = len(self._history) - 1 - - self._history[-1] = '' - self._edit.clear() - - if not text[-1] == '\n': - text += '\n' - - self.child_exec_command(text) - - def child_exec_command(self, text): - """ - Reimplement in the child classes - """ - pass - - def add_line_break_to_input(self): - self._edit.textCursor().insertText('\n') - - def append_output(self, text): - """Appent text to output widget - """ - self._append_to_browser('out', text) - - def append_error(self, text): - """Appent error text to output widget. Text is drawn with red background - """ - self._append_to_browser('err', text) - - def is_command_complete(self, text): - """ - Executed by _ExpandableTextEdit. Reimplement this function in the child classes. - """ - return True - - def browser(self): - return self._browser - - def _on_history_next(self): - """ - Down pressed, show next item from the history - """ - if (self._historyIndex + 1) < len(self._history): - self._historyIndex += 1 - self._edit.setPlainText(self._history[self._historyIndex]) - self._edit.moveCursor(QTextCursor.End) - - def _on_history_prev(self): - """ - Up pressed, show previous item from the history - """ - if self._historyIndex > 0: - if self._historyIndex == (len(self._history) - 1): - self._history[-1] = self._edit.toPlainText() - self._historyIndex -= 1 - self._edit.setPlainText(self._history[self._historyIndex]) - self._edit.moveCursor(QTextCursor.End) diff --git a/upgrade_geos.sh b/upgrade_geos.sh deleted file mode 100644 index 8c309a67..00000000 --- a/upgrade_geos.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -wget http://download.osgeo.org/geos/geos-3.4.2.tar.bz2 -tar xjvf geos-3.4.2.tar.bz2 -cd geos-3.4.2 -./configure --prefix=/usr -make -make install \ No newline at end of file