diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 43a140e3..d4f8f99b 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -22,6 +22,8 @@ import subprocess import tkinter as tk from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport +from PyQt5.QtCore import QSettings + import time # Just used for debugging. Double check before removing. import urllib.request, urllib.parse, urllib.error import webbrowser @@ -90,10 +92,13 @@ class App(QtCore.QObject): log.addHandler(handler) # Version - version = 8.906 - version_date = "2019/01/30" + version = 8.907 + version_date = "2019/02/6" beta = True + # current date now + date = str(datetime.today()).rpartition(' ')[0] + # URL for update checks and statistics version_url = "http://flatcam.org/version" @@ -264,8 +269,9 @@ class App(QtCore.QObject): self.FC_dark_blue = '#0000ffbf' QtCore.QObject.__init__(self) - self.ui = FlatCAMGUI(self.version, self.beta, self) + + # self.connect(self.ui, # QtCore.SIGNAL("geomUpdate(int, int, int, int, int)"), # self.save_geometry) PyQt4 @@ -299,6 +305,7 @@ class App(QtCore.QObject): "global_send_stats": self.general_defaults_form.general_app_group.send_stats_cb, "global_gridx": self.general_defaults_form.general_gui_group.gridx_entry, "global_gridy": self.general_defaults_form.general_gui_group.gridy_entry, + "global_snap_max": self.general_defaults_form.general_gui_group.snap_max_dist_entry, "global_plot_fill": self.general_defaults_form.general_gui_group.pf_color_entry, "global_plot_line": self.general_defaults_form.general_gui_group.pl_color_entry, "global_sel_fill": self.general_defaults_form.general_gui_group.sf_color_entry, @@ -343,6 +350,7 @@ class App(QtCore.QObject): "excellon_travelz": self.excellon_defaults_form.excellon_opt_group.travelz_entry, "excellon_feedrate": self.excellon_defaults_form.excellon_opt_group.feedrate_entry, "excellon_feedrate_rapid": self.excellon_defaults_form.excellon_opt_group.feedrate_rapid_entry, + "excellon_feedrate_probe": self.excellon_defaults_form.excellon_opt_group.feedrate_probe_entry, "excellon_spindlespeed": self.excellon_defaults_form.excellon_opt_group.spindlespeed_entry, "excellon_dwell": self.excellon_defaults_form.excellon_opt_group.dwell_cb, "excellon_dwelltime": self.excellon_defaults_form.excellon_opt_group.dwelltime_entry, @@ -350,6 +358,8 @@ class App(QtCore.QObject): "excellon_toolchangez": self.excellon_defaults_form.excellon_opt_group.toolchangez_entry, "excellon_toolchangexy": self.excellon_defaults_form.excellon_opt_group.toolchangexy_entry, "excellon_ppname_e": self.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb, + "excellon_z_pdepth": self.excellon_defaults_form.excellon_opt_group.pdepth_entry, + "excellon_f_plunge": self.excellon_defaults_form.excellon_opt_group.fplunge_cb, "excellon_startz": self.excellon_defaults_form.excellon_opt_group.estartz_entry, "excellon_endz": self.excellon_defaults_form.excellon_opt_group.eendz_entry, "excellon_tooldia": self.excellon_defaults_form.excellon_opt_group.tooldia_entry, @@ -367,10 +377,13 @@ class App(QtCore.QObject): "geometry_feedrate": self.geometry_defaults_form.geometry_opt_group.cncfeedrate_entry, "geometry_feedrate_z": self.geometry_defaults_form.geometry_opt_group.cncplunge_entry, "geometry_feedrate_rapid": self.geometry_defaults_form.geometry_opt_group.cncfeedrate_rapid_entry, + "geometry_feedrate_probe": self.geometry_defaults_form.geometry_opt_group.feedrate_probe_entry, "geometry_spindlespeed": self.geometry_defaults_form.geometry_opt_group.cncspindlespeed_entry, "geometry_dwell": self.geometry_defaults_form.geometry_opt_group.dwell_cb, "geometry_dwelltime": self.geometry_defaults_form.geometry_opt_group.dwelltime_entry, "geometry_ppname_g": self.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb, + "geometry_z_pdepth": self.geometry_defaults_form.geometry_opt_group.pdepth_entry, + "geometry_f_plunge": self.geometry_defaults_form.geometry_opt_group.fplunge_cb, "geometry_toolchange": self.geometry_defaults_form.geometry_opt_group.toolchange_cb, "geometry_toolchangez": self.geometry_defaults_form.geometry_opt_group.toolchangez_entry, "geometry_toolchangexy": self.geometry_defaults_form.geometry_opt_group.toolchangexy_entry, @@ -414,6 +427,18 @@ class App(QtCore.QObject): "tools_2sided_mirror_axis": self.tools_defaults_form.tools_2sided_group.mirror_axis_radio, "tools_2sided_axis_loc": self.tools_defaults_form.tools_2sided_group.axis_location_radio, "tools_2sided_drilldia": self.tools_defaults_form.tools_2sided_group.drill_dia_entry, + + "tools_film_type": self.tools_defaults_form.tools_film_group.film_type_radio, + "tools_film_boundary": self.tools_defaults_form.tools_film_group.film_boundary_entry, + "tools_film_scale": self.tools_defaults_form.tools_film_group.film_scale_entry, + + "tools_panelize_spacing_columns": self.tools_defaults_form.tools_panelize_group.pspacing_columns, + "tools_panelize_spacing_rows": self.tools_defaults_form.tools_panelize_group.pspacing_rows, + "tools_panelize_columns": self.tools_defaults_form.tools_panelize_group.pcolumns, + "tools_panelize_rows": self.tools_defaults_form.tools_panelize_group.prows, + "tools_panelize_constrain": self.tools_defaults_form.tools_panelize_group.pconstrain_cb, + "tools_panelize_constrainx": self.tools_defaults_form.tools_panelize_group.px_width_entry, + "tools_panelize_constrainy": self.tools_defaults_form.tools_panelize_group.py_height_entry } # loads postprocessors self.postprocessors = load_postprocessors(self) @@ -435,6 +460,8 @@ class App(QtCore.QObject): "global_send_stats": True, "global_gridx": 1.0, "global_gridy": 1.0, + "global_snap_max": 0.05, + "global_plot_fill": '#BBF268BF', "global_plot_line": '#006E20BF', "global_sel_fill": '#a5a5ffbf', @@ -448,7 +475,7 @@ class App(QtCore.QObject): # "global_pan_with_space_key": False, "global_workspace": False, "global_workspaceT": "A4P", - "global_toolbar_view": 31, + "global_toolbar_view": 127, "global_background_timeout": 300000, # Default value is 5 minutes "global_verbose_error_level": 0, # Shell verbosity 0 = default @@ -471,9 +498,9 @@ class App(QtCore.QObject): "global_shell_shape": [500, 300], # Shape of the shell in pixels. "global_shell_at_startup": False, # Show the shell at startup. "global_recent_limit": 10, # Max. items in recent list. - "fit_key": '1', - "zoom_out_key": '2', - "zoom_in_key": '3', + "fit_key": 'V', + "zoom_out_key": '-', + "zoom_in_key": '=', "grid_toggle_key": 'G', "zoom_ratio": 1.5, "global_point_clipboard_format": "(%.4f, %.4f)", @@ -510,6 +537,7 @@ class App(QtCore.QObject): "excellon_travelz": 0.1, "excellon_feedrate": 3.0, "excellon_feedrate_rapid": 3.0, + "excellon_feedrate_probe": 3.0, "excellon_spindlespeed": None, "excellon_dwell": False, "excellon_dwelltime": 1, @@ -521,6 +549,8 @@ class App(QtCore.QObject): "excellon_startz": None, "excellon_endz": 2.0, "excellon_ppname_e": 'default', + "excellon_z_pdepth": -0.02, + "excellon_f_plunge": False, "excellon_gcode_type": "drills", "geometry_plot": True, @@ -536,11 +566,14 @@ class App(QtCore.QObject): "geometry_feedrate": 3.0, "geometry_feedrate_z": 3.0, "geometry_feedrate_rapid": 3.0, + "geometry_feedrate_probe": 3.0, "geometry_cnctooldia": 0.016, "geometry_spindlespeed": None, "geometry_dwell": False, "geometry_dwelltime": 1, "geometry_ppname_g": 'default', + "geometry_z_pdepth": -0.02, + "geometry_f_plunge": False, "geometry_depthperpass": 0.002, "geometry_multidepth": False, "geometry_extracut": False, @@ -580,6 +613,17 @@ class App(QtCore.QObject): "tools_2sided_axis_loc": "point", "tools_2sided_drilldia": 1, + "tools_film_type": 'neg', + "tools_film_boundary": 1, + "tools_film_scale": 0, + + "tools_panelize_spacing_columns": 0, + "tools_panelize_spacing_rows": 0, + "tools_panelize_columns": 1, + "tools_panelize_rows": 1, + "tools_panelize_constrain": False, + "tools_panelize_constrainx": 0.0, + "tools_panelize_constrainy": 0.0 }) ############################### @@ -618,6 +662,7 @@ class App(QtCore.QObject): "units": self.general_options_form.general_app_group.units_radio, "global_gridx": self.general_options_form.general_gui_group.gridx_entry, "global_gridy": self.general_options_form.general_gui_group.gridy_entry, + "global_snap_max": self.general_options_form.general_gui_group.snap_max_dist_entry, "gerber_plot": self.gerber_options_form.gerber_gen_group.plot_cb, "gerber_solid": self.gerber_options_form.gerber_gen_group.solid_cb, @@ -654,6 +699,7 @@ class App(QtCore.QObject): "excellon_toolchangexy": self.excellon_options_form.excellon_opt_group.toolchangexy_entry, "excellon_tooldia": self.excellon_options_form.excellon_opt_group.tooldia_entry, "excellon_ppname_e": self.excellon_options_form.excellon_opt_group.pp_excellon_name_cb, + "excellon_f_plunge": self.excellon_options_form.excellon_opt_group.fplunge_cb, "excellon_startz": self.excellon_options_form.excellon_opt_group.estartz_entry, "excellon_endz": self.excellon_options_form.excellon_opt_group.eendz_entry, @@ -671,6 +717,7 @@ class App(QtCore.QObject): "geometry_dwell": self.geometry_options_form.geometry_opt_group.dwell_cb, "geometry_dwelltime": self.geometry_options_form.geometry_opt_group.dwelltime_entry, "geometry_ppname_g": self.geometry_options_form.geometry_opt_group.pp_geometry_name_cb, + "geometry_f_plunge": self.geometry_options_form.geometry_opt_group.fplunge_cb, "geometry_toolchange": self.geometry_options_form.geometry_opt_group.toolchange_cb, "geometry_toolchangez": self.geometry_options_form.geometry_opt_group.toolchangez_entry, "geometry_toolchangexy": self.geometry_options_form.geometry_opt_group.toolchangexy_entry, @@ -706,7 +753,20 @@ class App(QtCore.QObject): "tools_2sided_mirror_axis": self.tools_options_form.tools_2sided_group.mirror_axis_radio, "tools_2sided_axis_loc": self.tools_options_form.tools_2sided_group.axis_location_radio, - "tools_2sided_drilldia": self.tools_options_form.tools_2sided_group.drill_dia_entry + "tools_2sided_drilldia": self.tools_options_form.tools_2sided_group.drill_dia_entry, + + "tools_film_type": self.tools_options_form.tools_film_group.film_type_radio, + "tools_film_boundary": self.tools_options_form.tools_film_group.film_boundary_entry, + "tools_film_scale": self.tools_options_form.tools_film_group.film_scale_entry, + + "tools_panelize_spacing_columns": self.tools_options_form.tools_panelize_group.pspacing_columns, + "tools_panelize_spacing_rows": self.tools_options_form.tools_panelize_group.pspacing_rows, + "tools_panelize_columns": self.tools_options_form.tools_panelize_group.pcolumns, + "tools_panelize_rows": self.tools_options_form.tools_panelize_group.prows, + "tools_panelize_constrain": self.tools_options_form.tools_panelize_group.pconstrain_cb, + "tools_panelize_constrainx": self.tools_options_form.tools_panelize_group.px_width_entry, + "tools_panelize_constrainy": self.tools_options_form.tools_panelize_group.py_height_entry + } for name in list(self.postprocessors.keys()): @@ -719,6 +779,7 @@ class App(QtCore.QObject): "units": "IN", "global_gridx": 1.0, "global_gridy": 1.0, + "global_snap_max": 0.05, "global_background_timeout": 300000, # Default value is 5 minutes "global_verbose_error_level": 0, # Shell verbosity: # 0 = default(python trace only for unknown errors), @@ -759,6 +820,7 @@ class App(QtCore.QObject): "excellon_toolchangexy": "0.0, 0.0", "excellon_tooldia": 0.016, "excellon_ppname_e": 'default', + "excellon_f_plunge": False, "excellon_startz": None, "excellon_endz": 2.0, @@ -780,6 +842,7 @@ class App(QtCore.QObject): "geometry_startz": None, "geometry_endz": 2.0, "geometry_ppname_g": "default", + "geometry_f_plunge": False, "geometry_depthperpass": 0.002, "geometry_multidepth": False, "geometry_extracut": False, @@ -808,7 +871,19 @@ class App(QtCore.QObject): "tools_2sided_mirror_axis": "X", "tools_2sided_axis_loc": 'point', - "tools_2sided_drilldia": 1 + "tools_2sided_drilldia": 1, + + "tools_film_type": 'neg', + "tools_film_boundary": 1, + "tools_film_scale": 0, + + "tools_panelize_spacing_columns": 0, + "tools_panelize_spacing_rows": 0, + "tools_panelize_columns": 1, + "tools_panelize_rows": 1, + "tools_panelize_constrain": False, + "tools_panelize_constrainx": 0.0, + "tools_panelize_constrainy": 0.0 }) @@ -911,12 +986,6 @@ class App(QtCore.QObject): self.geo_editor = FlatCAMGeoEditor(self, disabled=True) self.exc_editor = FlatCAMExcEditor(self) - # start with GRID activated - self.ui.grid_snap_btn.trigger() - self.ui.corner_snap_btn.setEnabled(False) - self.ui.snap_max_dist_entry.setEnabled(False) - self.ui.g_editor_cmenu.setEnabled(False) - self.ui.e_editor_cmenu.setEnabled(False) #### Adjust tabs width #### # self.collection.view.setMinimumWidth(self.ui.options_scroll_area.widget().sizeHint().width() + @@ -1012,12 +1081,14 @@ class App(QtCore.QObject): self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) - self.ui.menuviewdisableall.triggered.connect(lambda: self.disable_plots(self.collection.get_list())) - self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(self.collection.get_non_selected())) - self.ui.menuviewenable.triggered.connect(lambda: self.enable_plots(self.collection.get_list())) + self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots) + self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots) + self.ui.menuviewenable.triggered.connect(self.enable_all_plots) self.ui.menuview_zoom_fit.triggered.connect(self.on_zoom_fit) self.ui.menuview_zoom_in.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5)) self.ui.menuview_zoom_out.triggered.connect(lambda: self.plotcanvas.zoom(1.5)) + self.ui.menuview_toggle_fscreen.triggered.connect(self.on_fullscreen) + self.ui.menuview_toggle_parea.triggered.connect(self.on_toggle_plotarea) self.ui.menuview_toggle_grid.triggered.connect(self.on_toggle_grid) self.ui.menuview_toggle_axis.triggered.connect(self.on_toggle_axis) self.ui.menuview_toggle_workspace.triggered.connect(self.on_workspace_menu) @@ -1069,10 +1140,12 @@ class App(QtCore.QObject): self.ui.gridmenu_3.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.2")) self.ui.gridmenu_4.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.5")) self.ui.gridmenu_5.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("1.0")) + self.ui.draw_line.triggered.connect(self.geo_editor.draw_tool_path) self.ui.draw_rect.triggered.connect(self.geo_editor.draw_tool_rectangle) self.ui.draw_cut.triggered.connect(self.geo_editor.cutpath) self.ui.draw_move.triggered.connect(self.geo_editor.on_move) + self.ui.drill.triggered.connect(self.exc_editor.exc_add_drill) self.ui.drill_array.triggered.connect(self.exc_editor.exc_add_drill_array) self.ui.drill_copy.triggered.connect(self.exc_editor.exc_copy_drills) @@ -1140,6 +1213,7 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified) self.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace) + self.general_defaults_form.general_gui_group.theme_combo.activated.connect(self.on_theme) # Modify G-CODE Plot Area TAB self.ui.code_editor.textChanged.connect(self.handleTextChanged) @@ -1219,6 +1293,7 @@ class App(QtCore.QObject): self.init_tcl() self.ui.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell") + self.ui.shell_dock.setObjectName('Shell_DockWidget') self.ui.shell_dock.setWidget(self.shell) self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) self.ui.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | @@ -1304,6 +1379,9 @@ class App(QtCore.QObject): # Variable to hold the status of the axis self.toggle_axis = True + # Variable to store the status of the fullscreen event + self.toggle_fscreen = False + self.cursor = None # Variable to store the GCODE that was edited @@ -1413,7 +1491,7 @@ class App(QtCore.QObject): self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit, before=self.ui.menueditorigin) - self.cutout_tool = ToolCutout(self) + self.cutout_tool = ToolCutOut(self) self.cutout_tool.install(icon=QtGui.QIcon('share/cut16.png'), pos=self.ui.menutool, before=self.measurement_tool.menuAction) @@ -1440,8 +1518,12 @@ class App(QtCore.QObject): self.log.debug("Tools are installed.") - def init_tools(self): + def remove_tools(self): + for act in self.ui.menutool.actions(): + self.ui.menutool.removeAction(act) + def init_tools(self): + log.debug("init_tools()") # delete the data currently in the Tools Tab and the Tab itself widget = QtWidgets.QTabWidget.widget(self.ui.notebook, 2) if widget is not None: @@ -1457,6 +1539,12 @@ class App(QtCore.QObject): self.ui.tool_tab_layout.addWidget(self.ui.tool_scroll_area) # reinstall all the Tools as some may have been removed when the data was removed from the Tools Tab + # first remove all of them + self.remove_tools() + # second re add the TCL Shell action to the Tools menu and reconnect it to ist slot function + self.ui.menutoolshell = self.ui.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line\tS') + self.ui.menutoolshell.triggered.connect(self.on_toggle_shell) + # third install all of them self.install_tools() self.log.debug("Tools are initialized.") @@ -1470,35 +1558,35 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("object2editor()") # adjust the visibility of some of the canvas context menu self.ui.popmenu_edit.setVisible(False) self.ui.popmenu_save.setVisible(True) - if isinstance(self.collection.get_active(), FlatCAMGeometry): - edited_object = self.collection.get_active() + edited_object = self.collection.get_active() + + if isinstance(edited_object, FlatCAMGeometry): # for now, if the Geometry is MultiGeo do not allow the editing if edited_object.multigeo is True: - self.inform.emit("[warning_notcl]Editing a MultiGeo Geometry is not possible for the moment.") + self.inform.emit("[WARNING_NOTCL]Editing a MultiGeo Geometry is not possible for the moment.") return - self.ui.update_obj_btn.setEnabled(True) + + # store the Geometry Editor Toolbar visibility before entering in the Editor + self.geo_editor.toolbar_old_state = True if self.ui.geo_edit_toolbar.isVisible() else False self.geo_editor.edit_fcgeometry(edited_object) - self.ui.g_editor_cmenu.setEnabled(True) # set call source to the Editor we go into self.call_source = 'geo_editor' - # prevent the user to change anything in the Selected Tab while the Geo Editor is active - sel_tab_widget_list = self.ui.selected_tab.findChildren(QtWidgets.QWidget) - for w in sel_tab_widget_list: - w.setEnabled(False) - elif isinstance(self.collection.get_active(), FlatCAMExcellon): - self.ui.update_obj_btn.setEnabled(True) - self.exc_editor.edit_exc_obj(self.collection.get_active()) - self.ui.e_editor_cmenu.setEnabled(True) + elif isinstance(edited_object, FlatCAMExcellon): + # store the Excellon Editor Toolbar visibility before entering in the Editor + self.exc_editor.toolbar_old_state = True if self.ui.exc_edit_toolbar.isVisible() else False + self.exc_editor.edit_fcexcellon(edited_object) + # set call source to the Editor we go into self.call_source = 'exc_editor' else: - self.inform.emit("[warning_notcl]Select a Geometry or Excellon Object to edit.") + self.inform.emit("[WARNING_NOTCL]Select a Geometry or Excellon Object to edit.") return # make sure that we can't select another object while in Editor Mode: @@ -1507,9 +1595,9 @@ class App(QtCore.QObject): # delete any selection shape that might be active as they are not relevant in Editor self.delete_selection_shape() - self.ui.plot_tab_area.setTabText(0, "EDITOR Area") - self.inform.emit("[warning_notcl]Editor is activated ...") + self.ui.plot_tab_area.protectTab(0) + self.inform.emit("[WARNING_NOTCL]Editor is activated ...") def editor2object(self): """ @@ -1517,6 +1605,7 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("editor2object()") # adjust the visibility of some of the canvas context menu self.ui.popmenu_edit.setVisible(True) @@ -1529,17 +1618,8 @@ class App(QtCore.QObject): obj_type = "Geometry" self.geo_editor.update_fcgeometry(edited_obj) self.geo_editor.update_options(edited_obj) - self.geo_editor.deactivate() - # edited_obj.on_tool_delete(all=True) - # edited_obj.on_tool_add(dia=edited_obj.options['cnctooldia']) - - self.ui.corner_snap_btn.setEnabled(False) - self.ui.update_obj_btn.setEnabled(False) - self.ui.g_editor_cmenu.setEnabled(False) - self.ui.e_editor_cmenu.setEnabled(False) - # update the geo object options so it is including the bounding box values try: xmin, ymin, xmax, ymax = edited_obj.bounds() @@ -1548,21 +1628,16 @@ class App(QtCore.QObject): edited_obj.options['xmax'] = xmax edited_obj.options['ymax'] = ymax except AttributeError: - self.inform.emit("[warning] Object empty after edit.") + self.inform.emit("[WARNING] Object empty after edit.") elif isinstance(edited_obj, FlatCAMExcellon): obj_type = "Excellon" - - self.exc_editor.update_exc_obj(edited_obj) - + self.exc_editor.update_fcexcellon(edited_obj) + self.exc_editor.update_options(edited_obj) self.exc_editor.deactivate() - self.ui.corner_snap_btn.setEnabled(False) - self.ui.update_obj_btn.setEnabled(False) - self.ui.g_editor_cmenu.setEnabled(False) - self.ui.e_editor_cmenu.setEnabled(False) else: - self.inform.emit("[warning_notcl]Select a Geometry or Excellon Object to update.") + self.inform.emit("[WARNING_NOTCL]Select a Geometry or Excellon Object to update.") return # restore the call_source to app @@ -1570,6 +1645,7 @@ class App(QtCore.QObject): edited_obj.plot() self.ui.plot_tab_area.setTabText(0, "Plot Area") + self.ui.plot_tab_area.protectTab(0) self.inform.emit("[success] %s is updated, returning to App..." % obj_type) # reset the Object UI to original settings @@ -1618,7 +1694,7 @@ class App(QtCore.QObject): """ pass - def shell_message(self, msg, show=False, error=False): + def shell_message(self, msg, show=False, error=False, warning=False, success=False): """ Shows a message on the FlatCAM Shell @@ -1633,7 +1709,13 @@ class App(QtCore.QObject): if error: self.shell.append_error(msg + "\n") else: - self.shell.append_output(msg + "\n") + if warning: + self.shell.append_warning(msg + "\n") + else: + if success: + self.shell.append_success(msg + "\n") + else: + self.shell.append_output(msg + "\n") except AttributeError: log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg)) @@ -1799,12 +1881,20 @@ class App(QtCore.QObject): msg_ = match.group(2) self.ui.fcinfo.set_status(str(msg_), level=level) - if level == "error" or level == "warning": + if level.lower() == "error": self.shell_message(msg, error=True, show=True) - elif level == "error_notcl" or level == "warning_notcl": + elif level.lower() == "warning": + self.shell_message(msg, warning=True, show=True) + + elif level.lower() == "error_notcl": self.shell_message(msg, error=True, show=False) + elif level.lower() == "warning_notcl": + self.shell_message(msg, warning=True, show=False) + + elif level.lower() == "success": + self.shell_message(msg, success=True, show=False) else: - self.shell_message(msg, error=False, show=False) + self.shell_message(msg, show=False) else: self.ui.fcinfo.set_status(str(msg), level="info") @@ -1836,6 +1926,16 @@ class App(QtCore.QObject): self.ui.toolbartools.setVisible(False) if tb & 16: + self.ui.exc_edit_toolbar.setVisible(True) + else: + self.ui.exc_edit_toolbar.setVisible(False) + + if tb & 32: + self.ui.geo_edit_toolbar.setVisible(True) + else: + self.ui.geo_edit_toolbar.setVisible(False) + + if tb & 64: self.ui.snap_toolbar.setVisible(True) else: self.ui.snap_toolbar.setVisible(False) @@ -1853,19 +1953,19 @@ class App(QtCore.QObject): f.close() except IOError: self.log.error("Could not load defaults file.") - self.inform.emit("[error] Could not load defaults file.") + self.inform.emit("[ERROR] Could not load defaults file.") # in case the defaults file can't be loaded, show all toolbars - self.defaults["global_toolbar_view"] = 31 + self.defaults["global_toolbar_view"] = 127 return try: defaults = json.loads(options) except: # in case the defaults file can't be loaded, show all toolbars - self.defaults["global_toolbar_view"] = 31 + self.defaults["global_toolbar_view"] = 127 e = sys.exc_info()[0] App.log.error(str(e)) - self.inform.emit("[error] Failed to parse defaults file.") + self.inform.emit("[ERROR] Failed to parse defaults file.") return self.defaults.update(defaults) log.debug("FlatCAM defaults loaded from: %s" % filename) @@ -1894,7 +1994,7 @@ class App(QtCore.QObject): filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]FlatCAM preferences import cancelled.") + self.inform.emit("[WARNING_NOTCL]FlatCAM preferences import cancelled.") else: try: f = open(filename) @@ -1902,7 +2002,7 @@ class App(QtCore.QObject): f.close() except IOError: self.log.error("Could not load defaults file.") - self.inform.emit("[error_notcl] Could not load defaults file.") + self.inform.emit("[ERROR_NOTCL] Could not load defaults file.") return try: @@ -1910,7 +2010,7 @@ class App(QtCore.QObject): except: e = sys.exc_info()[0] App.log.error(str(e)) - self.inform.emit("[error_notcl] Failed to parse defaults file.") + self.inform.emit("[ERROR_NOTCL] Failed to parse defaults file.") return self.defaults.update(defaults_from_file) self.inform.emit("[success]Imported Defaults from %s" %filename) @@ -1922,8 +2022,10 @@ class App(QtCore.QObject): filter = "Config File (*.FlatConfig);;All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export FlatCAM Preferences", - directory=self.data_path, filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export FlatCAM Preferences", + directory=self.data_path + '/preferences_' + self.date.replace('-', ''), filter=filter + ) except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export FlatCAM Preferences", filter=filter) @@ -1931,7 +2033,7 @@ class App(QtCore.QObject): defaults_from_file = {} if filename == "": - self.inform.emit("[warning_notcl]FlatCAM preferences export cancelled.") + self.inform.emit("[WARNING_NOTCL]FlatCAM preferences export cancelled.") return else: try: @@ -1947,7 +2049,7 @@ class App(QtCore.QObject): e = sys.exc_info()[0] App.log.error("Could not load defaults file.") App.log.error(str(e)) - self.inform.emit("[error_notcl]Could not load defaults file.") + self.inform.emit("[ERROR_NOTCL]Could not load defaults file.") return try: @@ -1966,11 +2068,13 @@ class App(QtCore.QObject): json.dump(defaults_from_file, f) f.close() except: - self.inform.emit("[error_notcl] Failed to write defaults to file.") + self.inform.emit("[ERROR_NOTCL] Failed to write defaults to file.") return self.inform.emit("[success]Exported Defaults to %s" % filename) def on_preferences_open_folder(self): + self.report_usage("on_preferences_open_folder()") + if sys.platform == 'win32': subprocess.Popen('explorer %s' % self.data_path) elif sys.platform == 'darwin': @@ -2014,7 +2118,7 @@ class App(QtCore.QObject): f = open(self.data_path + '/recent.json', 'w') except IOError: App.log.error("Failed to open recent items file for writing.") - self.inform.emit('[error_notcl]Failed to open recent files file for writing.') + self.inform.emit('[ERROR_NOTCL]Failed to open recent files file for writing.') return #try: @@ -2100,10 +2204,15 @@ class App(QtCore.QObject): try: return_value = initialize(obj, self) except Exception as e: - if str(e) == "Empty Geometry": - self.inform.emit("[error_notcl] Object (%s) failed because: %s" % (kind, str(e))) - else: - self.inform.emit("[error] Object (%s) failed because: %s" % (kind, str(e))) + msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n" + msg += "Object (%s) failed because: %s \n\n" % (kind, str(e)) + msg += traceback.format_exc() + self.inform.emit(msg) + + # if str(e) == "Empty Geometry": + # self.inform.emit("[ERROR_NOTCL] ) + # else: + # self.inform.emit("[ERROR] Object (%s) failed because: %s" % (kind, str(e))) return "fail" t2 = time.time() @@ -2141,6 +2250,8 @@ class App(QtCore.QObject): return obj def new_excellon_object(self): + self.report_usage("new_excellon_object()") + self.new_object('excellon', 'new_e', lambda x, y: None) def on_object_created(self, obj, plot, autoselect): @@ -2293,6 +2404,8 @@ class App(QtCore.QObject): self.save_defaults() def on_app_exit(self): + self.report_usage("on_app_exit()") + if self.collection.get_list(): msgbox = QtWidgets.QMessageBox() # msgbox.setText("Save changes ...") @@ -2335,7 +2448,7 @@ class App(QtCore.QObject): e = sys.exc_info()[0] App.log.error("Could not load defaults file.") App.log.error(str(e)) - self.inform.emit("[error_notcl] Could not load defaults file.") + self.inform.emit("[ERROR_NOTCL] Could not load defaults file.") return try: @@ -2344,7 +2457,7 @@ class App(QtCore.QObject): e = sys.exc_info()[0] App.log.error("Failed to parse defaults file.") App.log.error(str(e)) - self.inform.emit("[error_notcl] Failed to parse defaults file.") + self.inform.emit("[ERROR_NOTCL] Failed to parse defaults file.") return # Update options @@ -2358,7 +2471,7 @@ class App(QtCore.QObject): json.dump(defaults, f) f.close() except: - self.inform.emit("[error_notcl] Failed to write defaults to file.") + self.inform.emit("[ERROR_NOTCL] Failed to write defaults to file.") return # Save the toolbar view @@ -2375,9 +2488,15 @@ class App(QtCore.QObject): if self.ui.toolbartools.isVisible(): tb_status += 8 - if self.ui.snap_toolbar.isVisible(): + if self.ui.exc_edit_toolbar.isVisible(): tb_status += 16 + if self.ui.geo_edit_toolbar.isVisible(): + tb_status += 32 + + if self.ui.snap_toolbar.isVisible(): + tb_status += 64 + self.defaults["global_toolbar_view"] = tb_status if not silent: @@ -2402,7 +2521,7 @@ class App(QtCore.QObject): e = sys.exc_info()[0] App.log.error("Could not load factory defaults file.") App.log.error(str(e)) - self.inform.emit("[error_notcl] Could not load factory defaults file.") + self.inform.emit("[ERROR_NOTCL] Could not load factory defaults file.") return try: @@ -2411,7 +2530,7 @@ class App(QtCore.QObject): e = sys.exc_info()[0] App.log.error("Failed to parse factory defaults file.") App.log.error(str(e)) - self.inform.emit("[error_notcl] Failed to parse factory defaults file.") + self.inform.emit("[ERROR_NOTCL] Failed to parse factory defaults file.") return # Update options @@ -2425,7 +2544,7 @@ class App(QtCore.QObject): json.dump(factory_defaults, f_f_def_s) f_f_def_s.close() except: - self.inform.emit("[error_notcl] Failed to write factory defaults to file.") + self.inform.emit("[ERROR_NOTCL] Failed to write factory defaults to file.") return if silent is False: @@ -2459,6 +2578,7 @@ class App(QtCore.QObject): toggle shell if is visible close it if closed open it :return: """ + self.report_usage("on_toggle_shell()") if self.ui.shell_dock.isVisible(): self.ui.shell_dock.hide() @@ -2472,6 +2592,7 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("on_edit_join()") obj_name_single = str(name) if name else "Combo_SingleGeo" obj_name_multi = str(name) if name else "Combo_MultiGeo" @@ -2485,7 +2606,7 @@ class App(QtCore.QObject): # if len(set(geo_type_list)) == 1 means that all list elements are the same if len(set(geo_type_list)) != 1: - self.inform.emit("[error] Failed join. The Geometry objects are of different types.\n" + self.inform.emit("[ERROR] Failed join. The Geometry objects are of different types.\n" "At least one is MultiGeo type and the other is SingleGeo type. A possibility is to " "convert from one to another and retry joining \n" "but in the case of converting from MultiGeo to SingleGeo, informations may be lost and " @@ -2518,11 +2639,13 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("on_edit_join_exc()") + objs = self.collection.get_selected() for obj in objs: if not isinstance(obj, FlatCAMExcellon): - self.inform.emit("[error_notcl]Failed. Excellon joining works only on Excellon objects.") + self.inform.emit("[ERROR_NOTCL]Failed. Excellon joining works only on Excellon objects.") return def initialize(obj, app): @@ -2537,11 +2660,13 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("on_edit_join_grb()") + objs = self.collection.get_selected() for obj in objs: if not isinstance(obj, FlatCAMGerber): - self.inform.emit("[error_notcl]Failed. Gerber joining works only on Gerber objects.") + self.inform.emit("[ERROR_NOTCL]Failed. Gerber joining works only on Gerber objects.") return def initialize(obj, app): @@ -2550,14 +2675,16 @@ class App(QtCore.QObject): self.new_object("gerber", 'Combo_Gerber', initialize) def on_convert_singlegeo_to_multigeo(self): + self.report_usage("on_convert_singlegeo_to_multigeo()") + obj = self.collection.get_active() if obj is None: - self.inform.emit("[error_notcl]Failed. Select a Geometry Object and try again.") + self.inform.emit("[ERROR_NOTCL]Failed. Select a Geometry Object and try again.") return if not isinstance(obj, FlatCAMGeometry): - self.inform.emit("[error_notcl]Expected a FlatCAMGeometry, got %s" % type(obj)) + self.inform.emit("[ERROR_NOTCL]Expected a FlatCAMGeometry, got %s" % type(obj)) return obj.multigeo = True @@ -2571,14 +2698,16 @@ class App(QtCore.QObject): self.inform.emit("[success] A Geometry object was converted to MultiGeo type.") def on_convert_multigeo_to_singlegeo(self): + self.report_usage("on_convert_multigeo_to_singlegeo()") + obj = self.collection.get_active() if obj is None: - self.inform.emit("[error_notcl]Failed. Select a Geometry Object and try again.") + self.inform.emit("[ERROR_NOTCL]Failed. Select a Geometry Object and try again.") return if not isinstance(obj, FlatCAMGeometry): - self.inform.emit("[error_notcl]Expected a FlatCAMGeometry, got %s" % type(obj)) + self.inform.emit("[ERROR_NOTCL]Expected a FlatCAMGeometry, got %s" % type(obj)) return obj.multigeo = False @@ -2633,9 +2762,9 @@ class App(QtCore.QObject): 'excellon_travelz', 'excellon_feedrate', 'excellon_feedrate_rapid', 'excellon_toolchangez', 'excellon_tooldia', 'excellon_endz', 'cncjob_tooldia', 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate', 'geometry_feedrate_rapid', - 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap', 'geometry_toolchangexy', + 'geometry_cnctooldia', 'tools_painttooldia', 'tools_paintoverlap', 'geometry_toolchangexy', 'geometry_toolchangez', - 'geometry_paintmargin', 'geometry_endz', 'geometry_depthperpass', 'global_gridx', 'global_gridy'] + 'tools_paintmargin', 'geometry_endz', 'geometry_depthperpass', 'global_gridx', 'global_gridy'] def scale_options(sfactor): for dim in dimensions: @@ -2718,11 +2847,46 @@ class App(QtCore.QObject): self.on_toggle_units() def on_language_apply(self): + self.report_usage("on_language_apply()") + # TODO: apply the language # app restart section pass + def on_fullscreen(self): + self.report_usage("on_fullscreen()") + + if self.toggle_fscreen is False: + for tb in self.ui.findChildren(QtWidgets.QToolBar): + tb.setVisible(False) + self.ui.splitter_left.setVisible(False) + self.toggle_fscreen = True + else: + self.restore_toolbar_view() + self.ui.splitter_left.setVisible(True) + self.toggle_fscreen = False + + def on_toggle_plotarea(self): + self.report_usage("on_toggle_plotarea()") + + try: + name = self.ui.plot_tab_area.widget(0).objectName() + except AttributeError: + self.ui.plot_tab_area.addTab(self.ui.plot_tab, "Plot Area") + # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON + self.ui.plot_tab_area.protectTab(0) + return + + if name != 'plotarea': + self.ui.plot_tab_area.insertTab(0, self.ui.plot_tab, "Plot Area") + # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON + self.ui.plot_tab_area.protectTab(0) + else: + self.ui.plot_tab_area.closeTab(0) + def on_toggle_axis(self): + self.report_usage("on_toggle_axis()") + if self.toggle_axis is False: self.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0)) self.plotcanvas.h_line.set_data(color=(0.70, 0.3, 0.3, 1.0)) @@ -2736,6 +2900,8 @@ class App(QtCore.QObject): self.toggle_axis = False def on_toggle_grid(self): + self.report_usage("on_toggle_grid()") + self.ui.grid_snap_btn.trigger() def on_options_combo_change(self, sel): @@ -3058,6 +3224,8 @@ class App(QtCore.QObject): self.plotcanvas.draw_workspace() def on_workspace(self): + self.report_usage("on_workspace()") + if self.general_defaults_form.general_gui_group.workspace_cb.isChecked(): self.plotcanvas.restore_workspace() else: @@ -3072,6 +3240,98 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.workspace_cb.setChecked(True) self.on_workspace() + def on_theme(self): + self.report_usage("on_theme()") + + current_theme= self.general_defaults_form.general_gui_group.theme_combo.get_value().lower() + + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('theme', current_theme) + + # This will write the setting to the platform specific storage. + del settings + + # first remove the toolbars: + self.ui.removeToolBar(self.ui.toolbarfile) + self.ui.removeToolBar(self.ui.toolbargeo) + self.ui.removeToolBar(self.ui.toolbarview) + self.ui.removeToolBar(self.ui.toolbartools) + self.ui.removeToolBar(self.ui.exc_edit_toolbar) + self.ui.removeToolBar(self.ui.geo_edit_toolbar) + self.ui.removeToolBar(self.ui.snap_toolbar) + + if current_theme == 'standard': + ### TOOLBAR INSTALLATION ### + self.ui.toolbarfile = QtWidgets.QToolBar('File Toolbar') + self.ui.toolbarfile.setObjectName('File_TB') + self.ui.addToolBar(self.ui.toolbarfile) + + self.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar') + self.ui.toolbargeo.setObjectName('Edit_TB') + self.ui.addToolBar(self.ui.toolbargeo) + + self.ui.toolbarview = QtWidgets.QToolBar('View Toolbar') + self.ui.toolbarview.setObjectName('View_TB') + self.ui.addToolBar(self.ui.toolbarview) + + self.ui.toolbartools = QtWidgets.QToolBar('Tools Toolbar') + self.ui.toolbartools.setObjectName('Tools_TB') + self.ui.addToolBar(self.ui.toolbartools) + + self.ui.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar') + self.ui.exc_edit_toolbar.setVisible(False) + self.ui.exc_edit_toolbar.setObjectName('ExcEditor_TB') + self.ui.addToolBar(self.ui.exc_edit_toolbar) + + self.ui.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar') + self.ui.geo_edit_toolbar.setVisible(False) + self.ui.geo_edit_toolbar.setObjectName('GeoEditor_TB') + self.ui.addToolBar(self.ui.geo_edit_toolbar) + + self.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar') + self.ui.snap_toolbar.setObjectName('Snap_TB') + # self.ui.snap_toolbar.setMaximumHeight(30) + self.ui.addToolBar(self.ui.snap_toolbar) + + self.ui.corner_snap_btn.setVisible(False) + self.ui.snap_magnet.setVisible(False) + elif current_theme == 'compact': + ### TOOLBAR INSTALLATION ### + self.ui.toolbarfile = QtWidgets.QToolBar('File Toolbar') + self.ui.toolbarfile.setObjectName('File_TB') + self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbarfile) + self.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar') + self.ui.toolbargeo.setObjectName('Edit_TB') + self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbargeo) + self.ui.toolbarview = QtWidgets.QToolBar('View Toolbar') + self.ui.toolbarview.setObjectName('View_TB') + self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbarview) + self.ui.toolbartools = QtWidgets.QToolBar('Tools Toolbar') + self.ui.toolbartools.setObjectName('Tools_TB') + self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbartools) + self.ui.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar') + self.ui.exc_edit_toolbar.setObjectName('ExcEditor_TB') + self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.exc_edit_toolbar) + self.ui.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar') + self.ui.geo_edit_toolbar.setVisible(False) + self.ui.geo_edit_toolbar.setObjectName('GeoEditor_TB') + self.ui.addToolBar(Qt.RightToolBarArea, self.ui.geo_edit_toolbar) + self.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar') + self.ui.snap_toolbar.setObjectName('Snap_TB') + self.ui.snap_toolbar.setMaximumHeight(30) + self.ui.splitter_left.addWidget(self.ui.snap_toolbar) + + self.ui.corner_snap_btn.setVisible(True) + self.ui.snap_magnet.setVisible(True) + + self.ui.populate_toolbars() + + self.ui.grid_snap_btn.setChecked(True) + self.ui.grid_gap_x_entry.setText(str(self.defaults["global_gridx"])) + self.ui.grid_gap_y_entry.setText(str(self.defaults["global_gridy"])) + self.ui.snap_max_dist_entry.setText(str(self.defaults["global_snap_max"])) + self.ui.grid_gap_link_cb.setChecked(True) + def on_save_button(self): self.save_defaults(silent=False) # load the defaults so they are updated into the app @@ -3080,6 +3340,8 @@ class App(QtCore.QObject): self.on_options_app2project() def handleOpen(self): + self.report_usage("handleOpen()") + filter_group = " G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \ "All Files (*.*)" path, _ = QtWidgets.QFileDialog.getOpenFileName( @@ -3093,39 +3355,56 @@ class App(QtCore.QObject): file.close() def handlePrint(self): + self.report_usage("handlePrint()") + dialog = QtPrintSupport.QPrintDialog() if dialog.exec_() == QtWidgets.QDialog.Accepted: self.ui.code_editor.document().print_(dialog.printer()) def handlePreview(self): + self.report_usage("handlePreview()") + dialog = QtPrintSupport.QPrintPreviewDialog() dialog.paintRequested.connect(self.ui.code_editor.print_) dialog.exec_() def handleTextChanged(self): + self.report_usage("handleTextChanged()") + # enable = not self.ui.code_editor.document().isEmpty() # self.ui.buttonPrint.setEnabled(enable) # self.ui.buttonPreview.setEnabled(enable) pass def handleSaveGCode(self): + self.report_usage("handleSaveGCode()") + + obj_name = self.collection.get_active().options['name'] + _filter_ = " G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \ "All Files (*.*)" try: filename = str(QtWidgets.QFileDialog.getSaveFileName( - caption="Export G-Code ...", directory=self.defaults["global_last_folder"], filter=_filter_)[0]) + caption="Export G-Code ...", + directory=self.defaults["global_last_folder"] + '/' + str(obj_name), + filter=_filter_ + )[0]) except TypeError: filename = str(QtWidgets.QFileDialog.getSaveFileName(caption="Export G-Code ...", filter=_filter_)[0]) - try: - my_gcode = self.ui.code_editor.toPlainText() - with open(filename, 'w') as f: - for line in my_gcode: - f.write(line) - - except FileNotFoundError: - self.inform.emit("[WARNING] No such file or directory") + if filename == "": + self.inform.emit("[WARNING_NOTCL]Export CNC Code cancelled.") return + else: + try: + my_gcode = self.ui.code_editor.toPlainText() + with open(filename, 'w') as f: + for line in my_gcode: + f.write(line) + + except FileNotFoundError: + self.inform.emit("[WARNING] No such file or directory") + return # Just for adding it to the recent files list. self.file_opened.emit("cncjob", filename) @@ -3134,6 +3413,8 @@ class App(QtCore.QObject): self.inform.emit("Saved to: " + filename) def handleFindGCode(self): + self.report_usage("handleFindGCode()") + flags = QtGui.QTextDocument.FindCaseSensitively text_to_be_found = self.ui.entryFind.get_value() @@ -3141,8 +3422,9 @@ class App(QtCore.QObject): if r is False: self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) - def handleReplaceGCode(self): + self.report_usage("handleReplaceGCode()") + old = self.ui.entryFind.get_value() new = self.ui.entryReplace.get_value() @@ -3171,8 +3453,9 @@ class App(QtCore.QObject): # Mark end of undo block cursor.endEditBlock() - def on_new_geometry(self): + self.report_usage("on_new_geometry()") + def initialize(obj, self): obj.multitool = False @@ -3185,6 +3468,7 @@ class App(QtCore.QObject): :return: None """ + self.report_usage("on_delete()") # Make sure that the deletion will happen only after the Editor is no longer active otherwise we might delete # a geometry object before we update it. @@ -3213,6 +3497,8 @@ class App(QtCore.QObject): #display the message for the user #and ask him to click on the desired position + self.report_usage("on_set_origin()") + self.inform.emit('Click to set the origin ...') self.plotcanvas.vis_connect('mouse_press', self.on_set_zero_click) @@ -3223,6 +3509,7 @@ class App(QtCore.QObject): :return: """ + self.report_usage("on_jump_to()") dia_box = Dialog_box(title="Jump to Coordinates", label="Enter the coordinates in format X,Y:") @@ -3248,6 +3535,7 @@ class App(QtCore.QObject): self.inform.emit("Done.") def on_copy_object(self): + self.report_usage("on_copy_object()") def initialize(obj_init, app): obj_init.solid_geometry = obj.solid_geometry @@ -3316,6 +3604,8 @@ class App(QtCore.QObject): return "Operation failed: %s" % str(e) def on_rename_object(self, text): + self.report_usage("on_rename_object()") + named_obj = self.collection.get_active() for obj in named_obj: if obj is list: @@ -3327,6 +3617,7 @@ class App(QtCore.QObject): log.warning("Could not rename the object in the list") def on_copy_object_as_geometry(self): + self.report_usage("on_copy_object_as_geometry()") def initialize(obj_init, app): obj_init.solid_geometry = obj.solid_geometry @@ -3371,6 +3662,8 @@ class App(QtCore.QObject): self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click) def on_selectall(self): + self.report_usage("on_selectall()") + # delete the possible selection box around a possible selected object self.delete_selection_shape() for name in self.collection.get_names(): @@ -3393,6 +3686,8 @@ class App(QtCore.QObject): self.ui.show() def on_flipy(self): + self.report_usage("on_flipy()") + obj_list = self.collection.get_selected() xminlist = [] yminlist = [] @@ -3400,15 +3695,7 @@ class App(QtCore.QObject): ymaxlist = [] if not obj_list: - self.inform.emit("[warning_notcl] No object selected.") - msg = "Please Select an object to flip!" - warningbox = QtWidgets.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - warningbox.exec_() + self.inform.emit("[WARNING_NOTCL] No object selected to Flip on Y axis.") else: try: # first get a bounding box to fit all @@ -3434,10 +3721,12 @@ class App(QtCore.QObject): obj.plot() self.object_changed.emit(obj) except Exception as e: - self.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e)) + self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return def on_flipx(self): + self.report_usage("on_flipx()") + obj_list = self.collection.get_selected() xminlist = [] yminlist = [] @@ -3445,15 +3734,7 @@ class App(QtCore.QObject): ymaxlist = [] if not obj_list: - self.inform.emit("[warning_notcl] No object selected.") - msg = "Please Select an object to flip!" - warningbox = QtWidgets.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - warningbox.exec_() + self.inform.emit("[WARNING_NOTCL] No object selected to Flip on X axis.") else: try: # first get a bounding box to fit all @@ -3479,10 +3760,12 @@ class App(QtCore.QObject): obj.plot() self.object_changed.emit(obj) except Exception as e: - self.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e)) + self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return def on_rotate(self, silent=False, preset=None): + self.report_usage("on_rotate()") + obj_list = self.collection.get_selected() xminlist = [] yminlist = [] @@ -3490,15 +3773,7 @@ class App(QtCore.QObject): ymaxlist = [] if not obj_list: - self.inform.emit("[warning_notcl] No object selected.") - msg = "Please Select an object to rotate!" - warningbox = QtWidgets.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - warningbox.exec_() + self.inform.emit("[WARNING_NOTCL] No object selected to Rotate.") else: if silent is False: rotatebox = FCInputDialog(title="Transform", text="Enter the Angle value:", @@ -3531,24 +3806,18 @@ class App(QtCore.QObject): sel_obj.plot() self.object_changed.emit(sel_obj) except Exception as e: - self.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e)) + self.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) return def on_skewx(self): + self.report_usage("on_skewx()") + obj_list = self.collection.get_selected() xminlist = [] yminlist = [] if not obj_list: - self.inform.emit("[warning_notcl] No object selected.") - msg = "Please Select an object to skew/shear!" - warningbox = QtWidgets.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - warningbox.exec_() + self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on X axis.") else: skewxbox = FCInputDialog(title="Transform", text="Enter the Angle value:", min=-360, max=360, decimals=3) @@ -3570,20 +3839,14 @@ class App(QtCore.QObject): self.object_changed.emit(obj) def on_skewy(self): + self.report_usage("on_skewy()") + obj_list = self.collection.get_selected() xminlist = [] yminlist = [] if not obj_list: - self.inform.emit("[warning_notcl] No object selected.") - msg = "Please Select an object to skew/shear!" - warningbox = QtWidgets.QMessageBox() - warningbox.setText(msg) - warningbox.setWindowTitle("Warning ...") - warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - warningbox.exec_() + self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on Y axis.") else: skewybox = FCInputDialog(title="Transform", text="Enter the Angle value:", min=-360, max=360, decimals=3) @@ -3659,18 +3922,6 @@ class App(QtCore.QObject): if index.internalPointer().parent_item != self.collection.root_item: self.ui.notebook.setCurrentWidget(self.ui.selected_tab) - 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 - """ - - self.plotcanvas.fit_view() - def grid_status(self): if self.ui.grid_snap_btn.isChecked(): return 1 @@ -3721,9 +3972,23 @@ class App(QtCore.QObject): if event.key == 'S': self.on_file_saveproject() + # Toggle Plot Area + if event.key == 'F10': + self.on_toggle_plotarea() + return elif self.key_modifiers == QtCore.Qt.AltModifier: # place holder for further shortcut key + + if event.key == '1': + self.enable_all_plots() + + if event.key == '2': + self.disable_all_plots() + + if event.key == '3': + self.disable_other_plots() + if event.key == 'C': self.calculator_tool.run() @@ -3748,6 +4013,10 @@ class App(QtCore.QObject): if event.key == 'Z': self.panelize_tool.run() + if event.key == 'F10': + self.on_fullscreen() + + return elif self.key_modifiers == QtCore.Qt.ShiftModifier: # place holder for further shortcut key @@ -3782,7 +4051,6 @@ class App(QtCore.QObject): if event.key == 'Y': self.on_skewy() - return else: if event.key == 'F1': webbrowser.open(self.manual_url) @@ -3792,15 +4060,11 @@ class App(QtCore.QObject): webbrowser.open(self.video_url) return - if event.key == self.defaults['fit_key']: # 1 - self.on_zoom_fit(None) - return - - if event.key == self.defaults['zoom_out_key']: # 2 + if event.key == self.defaults['zoom_out_key']: # '-' self.plotcanvas.zoom(1 / self.defaults['zoom_ratio'], self.mouse) return - if event.key == self.defaults['zoom_in_key']: # 3 + if event.key == self.defaults['zoom_in_key']: # '=' self.plotcanvas.zoom(self.defaults['zoom_ratio'], self.mouse) return @@ -3813,6 +4077,15 @@ class App(QtCore.QObject): self.collection.get_active().ui.plot_cb.toggle() self.delete_selection_shape() + if event.key == '1': + self.on_select_tab('project') + + if event.key == '2': + self.on_select_tab('selected') + + if event.key == '3': + self.on_select_tab('tool') + if event.key == 'E': self.object2editor() @@ -3874,84 +4147,36 @@ class App(QtCore.QObject): return def on_shortcut_list(self): + self.report_usage("on_shortcut_list()") - msg = '''Shortcut list
-
-~: Show Shortcut List
-
-1: Zoom Fit
-2: Zoom Out
-3: Zoom In
-A: Draw an Arc (when in Edit Mode)
-C: Copy Geo Item (when in Edit Mode)
-E: Edit Object (if selected)
-G: Grid On/Off
-J: Jump to Coordinates
-L: New Excellon
-M: Move Obj
-M: Move Geo Item (when in Edit Mode)
-N: New Geometry
-N: Draw a Polygon (when in Edit Mode)
-O: Set Origin
-O: Draw a Circle (when in Edit Mode)
-Q: Change Units
-P: Open Properties Tool
-P: Draw a Path (when in Edit Mode)
-R: Rotate by 90 degree CW
-R: Draw Rectangle (when in Edit Mode)
-S: Shell Toggle
-V: View Fit
-X: Flip on X_axis
-Y: Flip on Y_axis
-
-Space: En(Dis)able Obj Plot
-CTRL+A: Select All
-CTRL+C: Copy Obj
-CTRL+E: Open Excellon File
-CTRL+G: Open Gerber File
-CTRL+N: New Project
-CTRL+M: Measurement Tool
-CTRL+O: Open Project
-CTRL+S: Save Project As
-CTRL+S: Save Object and Exit Editor (when in Edit Mode)
-
-SHIFT+C: Copy Obj_Name
-SHIFT+G: Toggle the axis
-SHIFT+P: Open Preferences Window
-SHIFT+R: Rotate by 90 degree CCW
-SHIFT+S: Run a Script
-SHIFT+W: Toggle the workspace
-SHIFT+X: Skew on X axis
-SHIFT+Y: Skew on Y axis
-
-ALT+C: Calculators Tool
-ALT+D: 2-Sided PCB Tool
-ALT+L: Film PCB Tool
-ALT+N: Non-Copper Clearing Tool
-ALT+P: Paint Area Tool
-ALT+R: Transformation Tool
-ALT+U: Cutout PCB Tool
-
-F1: Open Online Manual
-F2: Open Online Tutorials
-Del: Delete Obj -''' + # add the tab if it was closed + self.ui.plot_tab_area.addTab(self.ui.shortcuts_tab, "Key Shortcut List") - helpbox = QtWidgets.QMessageBox() - helpbox.setText(msg) - helpbox.setWindowTitle("Help") - helpbox.setWindowIcon(QtGui.QIcon('share/help.png')) - helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - helpbox.exec_() + # delete the absolute and relative position and messages in the infobar + self.ui.position_label.setText("") + self.ui.rel_position_label.setText("") + + # Switch plot_area to preferences page + self.ui.plot_tab_area.setCurrentWidget(self.ui.shortcuts_tab) + self.ui.show() + + def on_select_tab(self, name): + if name == 'project': + self.ui.notebook.setCurrentWidget(self.ui.project_tab) + elif name == 'selected': + self.ui.notebook.setCurrentWidget(self.ui.selected_tab) + elif name == 'tool': + self.ui.notebook.setCurrentWidget(self.ui.tool_tab) def on_copy_name(self): + self.report_usage("on_copy_name()") + obj = self.collection.get_active() try: name = obj.options["name"] except AttributeError: log.debug("on_copy_name() --> No object selected to copy it's name") - self.inform.emit("[warning_notcl]No object selected to copy it's name") + self.inform.emit("[WARNING_NOTCL]No object selected to copy it's name") return self.clipboard.setText(name) @@ -4241,7 +4466,7 @@ class App(QtCore.QObject): # TODO: on selected objects change the object colors and do not draw the selection box # self.plotcanvas.vispy_canvas.update() # this updates the canvas except Exception as e: - log.error("[error] Something went bad. %s" % str(e)) + log.error("[ERROR] Something went bad. %s" % str(e)) return def delete_selection_shape(self): @@ -4362,9 +4587,13 @@ class App(QtCore.QObject): self.ui.notebook.setCurrentWidget(self.ui.project_tab) def obj_properties(self): + self.report_usage("obj_properties()") + self.properties_tool.run() def obj_move(self): + self.report_usage("obj_move()") + self.move_tool.run() def on_fileopengerber(self): @@ -4396,7 +4625,7 @@ class App(QtCore.QObject): filenames = [str(filename) for filename in filenames] if len(filenames) == 0: - self.inform.emit("[warning_notcl]Open Gerber cancelled.") + self.inform.emit("[WARNING_NOTCL]Open Gerber cancelled.") else: for filename in filenames: if filename != '': @@ -4434,7 +4663,7 @@ class App(QtCore.QObject): follow = True if filename == "": - self.inform.emit("[warning_notcl]Open Gerber-Follow cancelled.") + self.inform.emit("[WARNING_NOTCL]Open Gerber-Follow cancelled.") else: self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename, follow]}) @@ -4461,7 +4690,7 @@ class App(QtCore.QObject): filenames = [str(filename) for filename in filenames] if len(filenames) == 0: - self.inform.emit("[warning_notcl]Open Excellon cancelled.") + self.inform.emit("[WARNING_NOTCL]Open Excellon cancelled.") else: for filename in filenames: if filename != '': @@ -4491,7 +4720,7 @@ class App(QtCore.QObject): filenames = [str(filename) for filename in filenames] if len(filenames) == 0: - self.inform.emit("[warning_notcl]Open G-Code cancelled.") + self.inform.emit("[WARNING_NOTCL]Open G-Code cancelled.") else: for filename in filenames: if filename != '': @@ -4520,7 +4749,7 @@ class App(QtCore.QObject): filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]Open Project cancelled.") + self.inform.emit("[WARNING_NOTCL]Open Project cancelled.") else: # self.worker_task.emit({'fcn': self.open_project, # 'params': [filename]}) @@ -4551,7 +4780,7 @@ class App(QtCore.QObject): # 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_notcl] Only Geometry, Gerber and CNCJob objects can be used." + msg = "[ERROR_NOTCL] Only Geometry, Gerber and CNCJob objects can be used." msgbox = QtWidgets.QMessageBox() msgbox.setInformativeText(msg) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) @@ -4563,15 +4792,17 @@ class App(QtCore.QObject): filter = "SVG File (*.svg);;All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG", - directory=self.get_last_save_folder(), filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export SVG", + directory=self.get_last_save_folder() + '/' + str(name), + filter=filter) except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG", filter=filter) filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]Export SVG cancelled.") + self.inform.emit("[WARNING_NOTCL]Export SVG cancelled.") return else: self.export_svg(name, filename) @@ -4585,15 +4816,17 @@ class App(QtCore.QObject): image = _screenshot() data = np.asarray(image) if not data.ndim == 3 and data.shape[-1] in (3, 4): - self.inform.emit('[[warning_notcl]] Data must be a 3D array with last dimension 3 or 4') + self.inform.emit('[[WARNING_NOTCL]] Data must be a 3D array with last dimension 3 or 4') return - filter = "PNG File (*.png);;All Files (*.*)" + filter_ = "PNG File (*.png);;All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export PNG Image", - directory=self.get_last_save_folder(), filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export PNG Image", + directory=self.get_last_save_folder() + '/png_' + str(self.date).replace('-', ''), + filter=filter_) except TypeError: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export PNG Image", filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export PNG Image", filter=filter_) filename = str(filename) @@ -4615,7 +4848,7 @@ class App(QtCore.QObject): obj = self.collection.get_active() if obj is None: - self.inform.emit("[warning_notcl] No object selected.") + self.inform.emit("[WARNING_NOTCL] No object selected.") msg = "Please Select an Excellon object to export" msgbox = QtWidgets.QMessageBox() msgbox.setInformativeText(msg) @@ -4626,7 +4859,7 @@ class App(QtCore.QObject): # Check for more compatible types and add as required if not isinstance(obj, FlatCAMExcellon): - msg = "[warning_notcl] Only Excellon objects can be used." + msg = "[WARNING_NOTCL] Only Excellon objects can be used." msgbox = QtWidgets.QMessageBox() msgbox.setInformativeText(msg) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) @@ -4638,18 +4871,20 @@ class App(QtCore.QObject): filter = "Excellon File (*.drl);;Excellon File (*.txt);;All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Excellon", - directory=self.get_last_save_folder(), filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export Excellon", + directory=self.get_last_save_folder() + '/' + name, + filter=filter) except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Excellon", filter=filter) filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]Export Excellon cancelled.") + self.inform.emit("[WARNING_NOTCL]Export Excellon cancelled.") return else: - if altium_format == None: + if altium_format is None: self.export_excellon(name, filename) self.file_saved.emit("Excellon", filename) else: @@ -4667,7 +4902,7 @@ class App(QtCore.QObject): obj = self.collection.get_active() if obj is None: - self.inform.emit("W[warning_notcl] No object selected.") + self.inform.emit("W[WARNING_NOTCL] No object selected.") msg = "Please Select a Geometry object to export" msgbox = QtWidgets.QMessageBox() msgbox.setInformativeText(msg) @@ -4678,7 +4913,7 @@ class App(QtCore.QObject): # Check for more compatible types and add as required if not isinstance(obj, FlatCAMGeometry): - msg = "[error_notcl] Only Geometry objects can be used." + msg = "[ERROR_NOTCL] Only Geometry objects can be used." msgbox = QtWidgets.QMessageBox() msgbox.setInformativeText(msg) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) @@ -4690,15 +4925,17 @@ class App(QtCore.QObject): filter = "DXF File (*.DXF);;All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export DXF", - directory=self.get_last_save_folder(), filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export DXF", + directory=self.get_last_save_folder() + '/' + name, + filter=filter) except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export DXF", filter=filter) filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]Export DXF cancelled.") + self.inform.emit("[WARNING_NOTCL]Export DXF cancelled.") return else: self.export_dxf(name, filename) @@ -4727,7 +4964,7 @@ class App(QtCore.QObject): filenames = [str(filename) for filename in filenames] if len(filenames) == 0: - self.inform.emit("[warning_notcl]Open SVG cancelled.") + self.inform.emit("[WARNING_NOTCL]Open SVG cancelled.") else: for filename in filenames: if filename != '': @@ -4757,7 +4994,7 @@ class App(QtCore.QObject): filenames = [str(filename) for filename in filenames] if len(filenames) == 0: - self.inform.emit("[warning_notcl]Open DXF cancelled.") + self.inform.emit("[WARNING_NOTCL]Open DXF cancelled.") else: for filename in filenames: if filename != '': @@ -4786,7 +5023,7 @@ class App(QtCore.QObject): filename = str(filename) if filename == "": - self.inform.emit("[warning_notcl]Open TCL script cancelled.") + self.inform.emit("[WARNING_NOTCL]Open TCL script cancelled.") else: try: with open(filename, "r") as tcl_script: @@ -4829,17 +5066,19 @@ class App(QtCore.QObject): self.report_usage("on_file_saveprojectas") - filter = "FlatCAM Project (*.FlatPrj);; All Files (*.*)" + filter_ = "FlatCAM Project (*.FlatPrj);; All Files (*.*)" try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Save Project As ...", - directory=self.get_last_save_folder(), filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Save Project As ...", + directory=self.get_last_save_folder() + '/Project_' + self.date.replace('-', ''), + filter=filter_) except TypeError: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Save Project As ...", filter=filter) + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Save Project As ...", filter=filter_) filename = str(filename) if filename == '': - self.inform.emit("[warning_notcl]Save Project cancelled.") + self.inform.emit("[WARNING_NOTCL]Save Project cancelled.") return try: @@ -4879,6 +5118,8 @@ class App(QtCore.QObject): :param filename: Path to the SVG file to save to. :return: """ + self.report_usage("export_svg()") + if filename is None: filename = self.defaults["global_last_save_folder"] @@ -4934,6 +5175,7 @@ class App(QtCore.QObject): :type: Bool :return: """ + self.report_usage("export_negative()") if filename is None: filename = self.defaults["global_last_save_folder"] @@ -4953,7 +5195,7 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % box_name if box is None: - self.inform.emit("[warning_notcl]No object Box. Using instead %s" % obj) + self.inform.emit("[WARNING_NOTCL]No object Box. Using instead %s" % obj) box = obj def make_negative_film(): @@ -5053,6 +5295,7 @@ class App(QtCore.QObject): :type: Bool :return: """ + self.report_usage("export_svg_black()") if filename is None: filename = self.defaults["global_last_save_folder"] @@ -5072,7 +5315,7 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % box_name if box is None: - self.inform.emit("[warning_notcl]No object Box. Using instead %s" % obj) + self.inform.emit("[WARNING_NOTCL]No object Box. Using instead %s" % obj) box = obj def make_black_film(): @@ -5164,6 +5407,8 @@ class App(QtCore.QObject): :param filename: Path to the Excellon file to save to. :return: """ + self.report_usage("export_excellon()") + if filename is None: filename = self.defaults["global_last_save_folder"] @@ -5240,14 +5485,14 @@ class App(QtCore.QObject): def job_thread_exc(app_obj): ret = make_excellon() if ret == 'fail': - self.inform.emit('[error_notcl] Could not export Excellon file.') + self.inform.emit('[ERROR_NOTCL] Could not export Excellon file.') return self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]}) else: ret = make_excellon() if ret == 'fail': - self.inform.emit('[error_notcl] Could not export Excellon file.') + self.inform.emit('[ERROR_NOTCL] Could not export Excellon file.') return def export_dxf(self, obj_name, filename, use_thread=True): @@ -5257,6 +5502,8 @@ class App(QtCore.QObject): :param filename: Path to the DXF file to save to. :return: """ + self.report_usage("export_dxf()") + if filename is None: filename = self.defaults["global_last_save_folder"] @@ -5296,14 +5543,14 @@ class App(QtCore.QObject): def job_thread_exc(app_obj): ret = make_dxf() if ret == 'fail': - self.inform.emit('[[warning_notcl]] Could not export DXF file.') + self.inform.emit('[[WARNING_NOTCL]] Could not export DXF file.') return self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]}) else: ret = make_dxf() if ret == 'fail': - self.inform.emit('[[warning_notcl]] Could not export DXF file.') + self.inform.emit('[[WARNING_NOTCL]] Could not export DXF file.') return def import_svg(self, filename, geo_type='geometry', outname=None): @@ -5315,13 +5562,15 @@ class App(QtCore.QObject): :param outname: :return: """ + self.report_usage("import_svg()") + obj_type = "" if geo_type is None or geo_type == "geometry": obj_type = "geometry" elif geo_type == "gerber": obj_type = geo_type else: - self.inform.emit("[error_notcl] Not supported type was choosed as parameter. " + self.inform.emit("[ERROR_NOTCL] Not supported type was choosed as parameter. " "Only Geometry and Gerber are supported") return @@ -5355,6 +5604,7 @@ class App(QtCore.QObject): :type putname: str :return: """ + self.report_usage("import_dxf()") obj_type = "" if geo_type is None or geo_type == "geometry": @@ -5362,7 +5612,7 @@ class App(QtCore.QObject): elif geo_type == "gerber": obj_type = geo_type else: - self.inform.emit("[error_notcl] Not supported type was choosed as parameter. " + self.inform.emit("[ERROR_NOTCL] Not supported type was choosed as parameter. " "Only Geometry and Gerber are supported") return @@ -5395,13 +5645,15 @@ class App(QtCore.QObject): :param outname: :return: """ + self.report_usage("import_image()") + obj_type = "" if type is None or type == "geometry": obj_type = "geometry" elif type == "gerber": obj_type = type else: - self.inform.emit("[error_notcl] Not supported type was picked as parameter. " + self.inform.emit("[ERROR_NOTCL] Not supported type was picked as parameter. " "Only Geometry and Gerber are supported") return @@ -5450,27 +5702,27 @@ class App(QtCore.QObject): try: gerber_obj.parse_file(filename, follow=follow) except IOError: - app_obj.inform.emit("[error_notcl] Failed to open file: " + filename) + app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: " + filename) app_obj.progress.emit(0) - self.inform.emit('[error_notcl] Failed to open file: ' + filename) + self.inform.emit('[ERROR_NOTCL] Failed to open file: ' + filename) return "fail" except ParseError as err: - app_obj.inform.emit("[error_notcl] Failed to parse file: " + filename + ". " + str(err)) + app_obj.inform.emit("[ERROR_NOTCL] Failed to parse file: " + filename + ". " + str(err)) app_obj.progress.emit(0) self.log.error(str(err)) return "fail" except: - msg = "[error] An internal error has ocurred. See shell.\n" + msg = "[ERROR] An internal error has ocurred. See shell.\n" msg += traceback.format_exc() app_obj.inform.emit(msg) return "fail" if gerber_obj.is_empty(): - # app_obj.inform.emit("[error] No geometry found in file: " + filename) + # app_obj.inform.emit("[ERROR] No geometry found in file: " + filename) # self.collection.set_active(gerber_obj.options["name"]) # self.collection.delete_active() - self.inform.emit("[error_notcl] Object is not Gerber file or empty. Aborting object creation.") + self.inform.emit("[ERROR_NOTCL] Object is not Gerber file or empty. Aborting object creation.") return "fail" # Further parsing @@ -5491,7 +5743,7 @@ class App(QtCore.QObject): ### Object creation ### ret = self.new_object("gerber", name, obj_init, autoselected=False) if ret == 'fail': - self.inform.emit('[error_notcl] Open Gerber failed. Probable not a Gerber file.') + self.inform.emit('[ERROR_NOTCL] Open Gerber failed. Probable not a Gerber file.') return # Register recent file @@ -5527,15 +5779,15 @@ class App(QtCore.QObject): ret = excellon_obj.parse_file(filename) if ret == "fail": log.debug("Excellon parsing failed.") - self.inform.emit("[error_notcl] This is not Excellon file.") + self.inform.emit("[ERROR_NOTCL] This is not Excellon file.") return "fail" except IOError: - app_obj.inform.emit("[error_notcl] Cannot open file: " + filename) + app_obj.inform.emit("[ERROR_NOTCL] Cannot open file: " + filename) log.debug("Could not open Excellon object.") self.progress.emit(0) # TODO: self and app_bjj mixed return "fail" except: - msg = "[error_notcl] An internal error has occurred. See shell.\n" + msg = "[ERROR_NOTCL] An internal error has occurred. See shell.\n" msg += traceback.format_exc() app_obj.inform.emit(msg) return "fail" @@ -5546,7 +5798,7 @@ class App(QtCore.QObject): return "fail" if excellon_obj.is_empty(): - app_obj.inform.emit("[error_notcl] No geometry found in file: " + filename) + app_obj.inform.emit("[ERROR_NOTCL] No geometry found in file: " + filename) return "fail" with self.proc_container.new("Opening Excellon."): @@ -5556,7 +5808,7 @@ class App(QtCore.QObject): ret = self.new_object("excellon", name, obj_init, autoselected=False) if ret == 'fail': - self.inform.emit('[error_notcl] Open Excellon file failed. Probable not an Excellon file.') + self.inform.emit('[ERROR_NOTCL] Open Excellon file failed. Probable not an Excellon file.') return # Register recent file @@ -5595,7 +5847,7 @@ class App(QtCore.QObject): gcode = f.read() f.close() except IOError: - app_obj_.inform.emit("[error_notcl] Failed to open " + filename) + app_obj_.inform.emit("[ERROR_NOTCL] Failed to open " + filename) self.progress.emit(0) return "fail" @@ -5605,7 +5857,7 @@ class App(QtCore.QObject): ret = job_obj.gcode_parse() if ret == "fail": - self.inform.emit("[error_notcl] This is not GCODE") + self.inform.emit("[ERROR_NOTCL] This is not GCODE") return "fail" self.progress.emit(60) @@ -5619,7 +5871,7 @@ class App(QtCore.QObject): # New object creation and file processing ret = self.new_object("cncjob", name, obj_init, autoselected=False) if ret == 'fail': - self.inform.emit("[error_notcl] Failed to create CNCJob Object. Probable not a GCode file.\n " + self.inform.emit("[ERROR_NOTCL] Failed to create CNCJob Object. Probable not a GCode file.\n " "Attempting to create a FlatCAM CNCJob Object from " "G-Code file failed during processing") return "fail" @@ -5653,14 +5905,14 @@ class App(QtCore.QObject): f = open(filename, 'r') except IOError: App.log.error("Failed to open project file: %s" % filename) - self.inform.emit("[error_notcl] Failed to open project file: %s" % filename) + self.inform.emit("[ERROR_NOTCL] 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_notcl] Failed to parse project file: %s" % filename) + self.inform.emit("[ERROR_NOTCL] Failed to parse project file: %s" % filename) f.close() return @@ -6011,14 +6263,14 @@ class App(QtCore.QObject): f = open(self.data_path + '/recent.json') except IOError: App.log.error("Failed to load recent item list.") - self.inform.emit("[error_notcl] Failed to load recent item list.") + self.inform.emit("[ERROR_NOTCL] 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_notcl] Failed to parse recent item list.") + self.inform.emit("[ERROR_NOTCL] Failed to parse recent item list.") f.close() return f.close() @@ -6075,9 +6327,74 @@ class App(QtCore.QObject): self.log.debug("Recent items list has been populated.") def setup_component_editor(self): - label = QtWidgets.QLabel("Choose an item from Project") - label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - self.ui.selected_scroll_area.setWidget(label) + # label = QtWidgets.QLabel("Choose an item from Project") + # label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) + + sel_title = QtWidgets.QTextEdit( + 'Shortcut Key List') + sel_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + sel_title.setFrameStyle(QtWidgets.QFrame.NoFrame) + # font = self.sel_title.font() + # font.setPointSize(12) + # self.sel_title.setFont(font) + + selected_text = ''' +

Selected Tab - Choose an Item from Project Tab

+ +

Details:
+The normal flow when working in FlatCAM is the following:

+ +
    +
  1. Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into FlatCAM using either the menu's, toolbars or key shortcuts.
    +
    + You can also load a FlatCAM project by double clicking on the project file, drag & drop of the file into the FLATCAM GUI or through the menu/toolbar links offered within the app.

    +  
  2. +
  3. Once an object is available in the Project Tab, by selecting it and then selecting SELECTED TAB (more simpler is to double click the object name in the Project Tab), SELECTED TAB will be updated with the object properties according to it's kind: Gerber, Excellon, Geometry or CNCJob object.
    +
    + If the selection of the object is done on the canvas by single click instead, and the SELECTED TAB is in focus, again the object properties will be displayed into the Selected Tab. Alternatively, double clicking on the object on the canvas will bring the SELECTED TAB and populate it even if it was out of focus.
    +
    + You can change the parameters in this screen and the flow direction is like this:
    +
    + Gerber/Excellon Object -> Change Param -> Generate Geometry -> Geometry Object -> Add tools (change param in Selected Tab) -> Generate CNCJob -> CNCJob Object -> Verify GCode (through Edit CNC Code) and/or append/prepend to GCode (again, done in SELECTED TAB) -> Save GCode
  4. +
+ +

A list of key shortcuts is available through an menu entry in Help -> Shortcuts List or through it's own key shortcut: '`' (key left to 1).

+ + ''' + + sel_title.setText(selected_text) + sel_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + self.ui.selected_scroll_area.setWidget(sel_title) + + tool_title = QtWidgets.QTextEdit( + 'Shortcut Key List') + tool_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + tool_title.setFrameStyle(QtWidgets.QFrame.NoFrame) + # font = self.sel_title.font() + # font.setPointSize(12) + # self.sel_title.setFont(font) + + tool_text = ''' +

Tool Tab - Choose an Item in Tools Menu

+ +

Details:
+Some of the functionality of FlatCAM have been implemented as tools (a sort of plugins).

+ +

Most of the tools are accessible through the Tools menu or by using the associated shortcut keys.
+Each such a tool, if it needs an object to be used as a source it will provide the way to select this object(s) through a series of comboboxes. The result of using a tool is either a Geometry, an information that can be used in the app or it can be a file that can be saved.

+ +
    +
+ +

A list of key shortcuts is available through an menu entry in Help -> Shortcuts List or through it's own key shortcut: '`' (key left to 1).

+ + ''' + + tool_title.setText(tool_text) + tool_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + self.ui.tool_scroll_area.setWidget(tool_title) def setup_obj_classes(self): """ @@ -6125,14 +6442,14 @@ class App(QtCore.QObject): 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_notcl] Failed checking for latest version. Could not connect.") + self.inform.emit("[WARNING_NOTCL] 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_notcl] Could not parse information about latest version.") + self.inform.emit("[ERROR_NOTCL] Could not parse information about latest version.") App.log.debug("json.load(): %s" % str(e)) f.close() return @@ -6155,6 +6472,36 @@ class App(QtCore.QObject): "info" ) + 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 + """ + + self.plotcanvas.fit_view() + + def disable_all_plots(self): + self.report_usage("disable_all_plots()") + + self.disable_plots(self.collection.get_list()) + self.inform.emit("[success]All plots disabled.") + + def disable_other_plots(self): + self.report_usage("disable_other_plots()") + + self.disable_plots(self.collection.get_non_selected()) + self.inform.emit("[success]All non selected plots disabled.") + + def enable_all_plots(self): + self.report_usage("enable_all_plots()") + + self.enable_plots(self.collection.get_list()) + self.inform.emit("[success]All plots enabled.") + # TODO: FIX THIS ''' By default this is not threaded @@ -6246,6 +6593,8 @@ class App(QtCore.QObject): self.clear_pool() def generate_cnc_job(self, objects): + self.report_usage("generate_cnc_job()") + for obj in objects: obj.generatecncjob() @@ -6265,7 +6614,7 @@ class App(QtCore.QObject): try: self.collection.get_active().read_form() except: - self.log.debug("[warning] There was no active object") + self.log.debug("[WARNING] There was no active object") pass # Project options self.options_read_form() @@ -6279,7 +6628,7 @@ class App(QtCore.QObject): try: f = open(filename, 'w') except IOError: - App.log.error("[error] Failed to open file for saving: %s", filename) + App.log.error("[ERROR] Failed to open file for saving: %s", filename) return # Write @@ -6291,13 +6640,13 @@ class App(QtCore.QObject): try: saved_f = open(filename, 'r') except IOError: - self.inform.emit("[error_notcl] Failed to verify project file: %s. Retry to save it." % filename) + self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) return try: saved_d = json.load(saved_f, object_hook=dict2obj) except: - self.inform.emit("[error_notcl] Failed to parse saved project file: %s. Retry to save it." % filename) + self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) f.close() return saved_f.close() @@ -6305,7 +6654,7 @@ class App(QtCore.QObject): if 'version' in saved_d: self.inform.emit("[success] Project saved to: %s" % filename) else: - self.inform.emit("[error_notcl] Failed to save project file: %s. Retry to save it." % filename) + self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) def on_options_app2project(self): """ diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index 49915e2f..a472da6b 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -7,7 +7,7 @@ ############################################################ from PyQt5 import QtGui, QtCore, QtWidgets -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QSettings import FlatCAMApp from camlib import * from FlatCAMTool import FlatCAMTool @@ -590,6 +590,7 @@ class FCCircle(FCShapeTool): self.points.append(point) if len(self.points) == 1: + self.draw_app.app.inform.emit("Click on Circle perimeter point to complete ...") return "Click on perimeter to complete ..." if len(self.points) == 2: @@ -638,9 +639,11 @@ class FCArc(FCShapeTool): self.points.append(point) if len(self.points) == 1: + self.draw_app.app.inform.emit("Click on Start arc point ...") return "Click on 1st point ..." if len(self.points) == 2: + self.draw_app.app.inform.emit("Click on End arc point to complete ...") return "Click on 2nd point to complete ..." if len(self.points) == 3: @@ -850,6 +853,7 @@ class FCPolygon(FCShapeTool): self.points.append(point) if len(self.points) > 0: + self.draw_app.app.inform.emit("Click on next Point or click Right mouse button to complete ...") return "Click on next point or hit ENTER to complete ..." return "" @@ -1239,7 +1243,7 @@ class FCText(FCShapeTool): self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy)) except Exception as e: log.debug("Font geometry is empty or incorrect: %s" % str(e)) - self.draw_app.app.inform.emit("[error]Font not supported. Only Regular, Bold, Italic and BoldItalic are " + self.draw_app.app.inform.emit("[ERROR]Font not supported. Only Regular, Bold, Italic and BoldItalic are " "supported. Error: %s" % str(e)) self.text_gui.text_path = [] self.text_gui.hide_tool() @@ -1416,7 +1420,7 @@ class FCDrillAdd(FCShapeTool): self.draw_app.tools_table_exc.setCurrentItem(item) except KeyError: - self.draw_app.app.inform.emit("[warning_notcl] To add a drill first select a tool") + self.draw_app.app.inform.emit("[WARNING_NOTCL] To add a drill first select a tool") self.draw_app.select_tool("select") return @@ -1500,7 +1504,7 @@ class FCDrillArray(FCShapeTool): item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1) self.draw_app.tools_table_exc.setCurrentItem(item) except KeyError: - self.draw_app.app.inform.emit("[warning_notcl] To add an Drill Array first select a tool in Tool Table") + self.draw_app.app.inform.emit("[WARNING_NOTCL] To add an Drill Array first select a tool in Tool Table") return geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True) @@ -1525,7 +1529,7 @@ class FCDrillArray(FCShapeTool): self.flag_for_circ_array = True self.set_origin(point) - self.draw_app.app.inform.emit("Click on the circular array Start position") + self.draw_app.app.inform.emit("Click on the Drill Circular Array Start position") else: self.destination = point self.make() @@ -1547,10 +1551,10 @@ class FCDrillArray(FCShapeTool): self.drill_angle = float(self.draw_app.drill_angle_entry.get_value()) except TypeError: self.draw_app.app.inform.emit( - "[error_notcl] The value is not Float. Check for comma instead of dot separator.") + "[ERROR_NOTCL] The value is not Float. Check for comma instead of dot separator.") return except Exception as e: - self.draw_app.app.inform.emit("[error_notcl] The value is mistyped. Check the value.") + self.draw_app.app.inform.emit("[ERROR_NOTCL] The value is mistyped. Check the value.") return if self.drill_array == 'Linear': @@ -1630,7 +1634,7 @@ class FCDrillArray(FCShapeTool): self.geometry.append(DrawToolShape(geo)) else: if (self.drill_angle * self.drill_array_size) > 360: - self.draw_app.app.inform.emit("[warning_notcl]Too many drills for the selected spacing angle.") + self.draw_app.app.inform.emit("[WARNING_NOTCL]Too many drills for the selected spacing angle.") return radius = distance(self.destination, self.origin) @@ -1676,7 +1680,7 @@ class FCDrillResize(FCShapeTool): try: new_dia = self.draw_app.resdrill_entry.get_value() except: - self.draw_app.app.inform.emit("[error_notcl]Resize drill(s) failed. Please enter a diameter for resize.") + self.draw_app.app.inform.emit("[ERROR_NOTCL]Resize drill(s) failed. Please enter a diameter for resize.") return if new_dia not in self.draw_app.olddia_newdia: @@ -1890,9 +1894,6 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app = app self.canvas = app.plotcanvas - self.app.ui.geo_edit_toolbar.setDisabled(disabled) - self.app.ui.snap_max_dist_entry.setDisabled(disabled) - self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle')) self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc')) self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle')) @@ -1969,6 +1970,9 @@ class FlatCAMGeoEditor(QtCore.QObject): self.move_timer = QtCore.QTimer() self.move_timer.setSingleShot(True) + # this var will store the state of the toolbar before starting the editor + self.toolbar_old_state = False + self.key = None # Currently pressed key self.geo_key_modifiers = None self.x = None # Current mouse cursor pos @@ -1991,12 +1995,13 @@ class FlatCAMGeoEditor(QtCore.QObject): self.tools[tool]["button"].setCheckable(True) # Checkable self.app.ui.grid_snap_btn.triggered.connect(self.on_grid_toggled) + self.app.ui.corner_snap_btn.setCheckable(True) self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap")) self.options = { "global_gridx": 0.1, "global_gridy": 0.1, - "snap_max": 0.05, + "global_snap_max": 0.05, "grid_snap": True, "corner_snap": False, "grid_gap_link": True @@ -2009,7 +2014,7 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.ui.grid_gap_x_entry.setText(str(self.options["global_gridx"])) self.app.ui.grid_gap_y_entry.setText(str(self.options["global_gridy"])) - self.app.ui.snap_max_dist_entry.setText(str(self.options["snap_max"])) + self.app.ui.snap_max_dist_entry.setText(str(self.options["global_snap_max"])) self.app.ui.grid_gap_link_cb.setChecked(True) self.rtree_index = rtindex.Index() @@ -2048,10 +2053,27 @@ class FlatCAMGeoEditor(QtCore.QObject): self.shapes.enabled = True self.tool_shape.enabled = True self.app.app_cursor.enabled = True - self.app.ui.snap_max_dist_entry.setDisabled(False) + + self.app.ui.snap_max_dist_entry.setEnabled(True) self.app.ui.corner_snap_btn.setEnabled(True) + self.app.ui.snap_magnet.setVisible(True) + self.app.ui.corner_snap_btn.setVisible(True) self.app.ui.geo_editor_menu.setDisabled(False) + self.app.ui.geo_editor_menu.menuAction().setVisible(True) + + self.app.ui.update_obj_btn.setEnabled(True) + self.app.ui.g_editor_cmenu.setEnabled(True) + + self.app.ui.geo_edit_toolbar.setDisabled(False) + self.app.ui.geo_edit_toolbar.setVisible(True) + self.app.ui.snap_toolbar.setDisabled(False) + + # prevent the user to change anything in the Selected Tab while the Geo Editor is active + sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget) + for w in sel_tab_widget_list: + w.setEnabled(False) + # Tell the App that the editor is active self.editor_active = True @@ -2059,11 +2081,33 @@ class FlatCAMGeoEditor(QtCore.QObject): self.disconnect_canvas_event_handlers() self.clear() self.app.ui.geo_edit_toolbar.setDisabled(True) - self.app.ui.geo_edit_toolbar.setVisible(False) - self.app.ui.snap_max_dist_entry.setDisabled(True) - self.app.ui.corner_snap_btn.setEnabled(False) - # never deactivate the snap toolbar - MS - # self.app.ui.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("theme"): + theme = settings.value('theme', type=str) + if theme == 'standard': + # self.app.ui.geo_edit_toolbar.setVisible(False) + + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + self.app.ui.snap_magnet.setVisible(False) + self.app.ui.corner_snap_btn.setVisible(False) + elif theme == 'compact': + # self.app.ui.geo_edit_toolbar.setVisible(True) + + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + else: + # self.app.ui.geo_edit_toolbar.setVisible(False) + + self.app.ui.snap_magnet.setVisible(False) + self.app.ui.corner_snap_btn.setVisible(False) + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + + # set the Editor Toolbar visibility to what was before entering in the Editor + self.app.ui.geo_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \ + else self.app.ui.geo_edit_toolbar.setVisible(True) # Disable visuals self.shapes.enabled = False @@ -2071,6 +2115,13 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.app_cursor.enabled = False self.app.ui.geo_editor_menu.setDisabled(True) + self.app.ui.geo_editor_menu.menuAction().setVisible(False) + + self.app.ui.update_obj_btn.setEnabled(False) + + self.app.ui.g_editor_cmenu.setEnabled(False) + self.app.ui.e_editor_cmenu.setEnabled(False) + # Tell the app that the editor is no longer active self.editor_active = False @@ -2230,9 +2281,7 @@ class FlatCAMGeoEditor(QtCore.QObject): self.add_shape(DrawToolShape(shape)) self.replot() - self.app.ui.geo_edit_toolbar.setDisabled(False) - self.app.ui.geo_edit_toolbar.setVisible(True) - self.app.ui.snap_toolbar.setDisabled(False) + # start with GRID toolbar activated if self.app.ui.grid_snap_btn.isChecked() == False: @@ -2561,7 +2610,7 @@ class FlatCAMGeoEditor(QtCore.QObject): if event.key.name == 'Escape': # TODO: ...? # self.on_tool_select("select") - self.app.inform.emit("[warning_notcl]Cancelled.") + self.app.inform.emit("[WARNING_NOTCL]Cancelled.") self.delete_utility_geometry() @@ -2723,47 +2772,7 @@ class FlatCAMGeoEditor(QtCore.QObject): # Show Shortcut list if event.key.name == '`': - self.on_shortcut_list() - - def on_shortcut_list(self): - msg = '''Shortcut list in Geometry Editor
-
-1: Zoom Fit
-2: Zoom Out
-3: Zoom In
-A: Add an 'Arc'
-B: Add a Buffer Geo
-C: Copy Geo Item
-E: Intersection Tool
-G: Grid Snap On/Off
-I: Paint Tool
-K: Corner Snap On/Off
-M: Move Geo Item
-
-N: Add an 'Polygon'
-O: Add a 'Circle'
-P: Add a 'Path'
-R: Add an 'Rectangle'
-S: Substraction Tool
-T: Add Text Geometry
-U: Union Tool
-
-X: Cut Path
-
-~: Show Shortcut List
-
-Space: Rotate selected Geometry
-Enter: Finish Current Action
-Escape: Select Tool (Exit any other Tool)
-Delete: Delete Obj''' - - helpbox =QtWidgets.QMessageBox() - helpbox.setText(msg) - helpbox.setWindowTitle("Help") - helpbox.setWindowIcon(QtGui.QIcon('share/help.png')) - helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - helpbox.exec_() + self.app.on_shortcut_list() def on_canvas_key_release(self, event): self.key = None @@ -2945,7 +2954,7 @@ class FlatCAMGeoEditor(QtCore.QObject): nearest_pt, shape = self.storage.nearest((x, y)) nearest_pt_distance = distance((x, y), nearest_pt) - if nearest_pt_distance <= self.options["snap_max"]: + if nearest_pt_distance <= float(self.options["global_snap_max"]): snap_distance = nearest_pt_distance snap_x, snap_y = nearest_pt except (StopIteration, AssertionError): @@ -3037,7 +3046,7 @@ class FlatCAMGeoEditor(QtCore.QObject): results = shapes[0].geo except Exception as e: log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e)) - self.app.inform.emit("[warning_notcl]A selection of at least 2 geo items is required to do Intersection.") + self.app.inform.emit("[WARNING_NOTCL]A selection of at least 2 geo items is required to do Intersection.") self.select_tool('select') return @@ -3075,7 +3084,7 @@ class FlatCAMGeoEditor(QtCore.QObject): if buf_distance < 0: self.app.inform.emit( - "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape") + "[ERROR_NOTCL]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape") # deselect everything self.selected = [] @@ -3083,11 +3092,11 @@ class FlatCAMGeoEditor(QtCore.QObject): return if len(selected) == 0: - self.app.inform.emit("[warning_notcl] Nothing selected for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.") return if not isinstance(buf_distance, float): - self.app.inform.emit("[warning_notcl] Invalid distance for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.") # deselect everything self.selected = [] @@ -3097,7 +3106,7 @@ class FlatCAMGeoEditor(QtCore.QObject): pre_buffer = cascaded_union([t.geo for t in selected]) results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style) if results.is_empty: - self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.") + self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a different buffer value.") # deselect everything self.selected = [] self.replot() @@ -3112,18 +3121,18 @@ class FlatCAMGeoEditor(QtCore.QObject): if buf_distance < 0: self.app.inform.emit( - "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape") + "[ERROR_NOTCL]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape") # deselect everything self.selected = [] self.replot() return if len(selected) == 0: - self.app.inform.emit("[warning_notcl] Nothing selected for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.") return if not isinstance(buf_distance, float): - self.app.inform.emit("[warning_notcl] Invalid distance for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.") # deselect everything self.selected = [] self.replot() @@ -3132,7 +3141,7 @@ class FlatCAMGeoEditor(QtCore.QObject): pre_buffer = cascaded_union([t.geo for t in selected]) results = pre_buffer.buffer(-buf_distance + 1e-10, resolution=32, join_style=join_style) if results.is_empty: - self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a smaller buffer value.") + self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a smaller buffer value.") # deselect everything self.selected = [] self.replot() @@ -3152,7 +3161,7 @@ class FlatCAMGeoEditor(QtCore.QObject): # return # # if not isinstance(buf_distance, float): - # self.app.inform.emit("[warning] Invalid distance for buffering.") + # self.app.inform.emit("[WARNING] Invalid distance for buffering.") # return # # pre_buffer = cascaded_union([t.geo for t in selected]) @@ -3182,7 +3191,7 @@ class FlatCAMGeoEditor(QtCore.QObject): selected = self.get_selected() if buf_distance < 0: - self.app.inform.emit("[error_notcl]Negative buffer value is not accepted. " + self.app.inform.emit("[ERROR_NOTCL]Negative buffer value is not accepted. " "Use Buffer interior to generate an 'inside' shape") # deselect everything self.selected = [] @@ -3190,11 +3199,11 @@ class FlatCAMGeoEditor(QtCore.QObject): return if len(selected) == 0: - self.app.inform.emit("[warning_notcl] Nothing selected for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.") return if not isinstance(buf_distance, float): - self.app.inform.emit("[warning_notcl] Invalid distance for buffering.") + self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.") # deselect everything self.selected = [] self.replot() @@ -3203,7 +3212,7 @@ class FlatCAMGeoEditor(QtCore.QObject): pre_buffer = cascaded_union([t.geo for t in selected]) results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style) if results.is_empty: - self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.") + self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a different buffer value.") # deselect everything self.selected = [] self.replot() @@ -3221,13 +3230,13 @@ class FlatCAMGeoEditor(QtCore.QObject): # selected = self.get_selected() # # if len(selected) == 0: - # self.app.inform.emit("[warning] Nothing selected for painting.") + # 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 locals().items() if v is param][0] - # self.app.inform.emit("[warning] Invalid value for {}".format(param)) + # self.app.inform.emit("[WARNING] Invalid value for {}".format(param)) # # # Todo: Check for valid method. # @@ -3279,19 +3288,19 @@ class FlatCAMGeoEditor(QtCore.QObject): selected = self.get_selected() if len(selected) == 0: - self.app.inform.emit("[warning_notcl]Nothing selected for painting.") + self.app.inform.emit("[WARNING_NOTCL]Nothing selected for painting.") return for param in [tooldia, overlap, margin]: if not isinstance(param, float): param_name = [k for k, v in locals().items() if v is param][0] - self.app.inform.emit("[warning] Invalid value for {}".format(param)) + self.app.inform.emit("[WARNING] Invalid value for {}".format(param)) results = [] if tooldia >= overlap: self.app.inform.emit( - "[error_notcl] Could not do Paint. Overlap value has to be less than Tool Dia value.") + "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than Tool Dia value.") return def recurse(geometry, reset=True): @@ -3350,7 +3359,7 @@ class FlatCAMGeoEditor(QtCore.QObject): except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit( - "[error] Could not do Paint. Try a different combination of parameters. " + "[ERROR] Could not do Paint. Try a different combination of parameters. " "Or a different method of Paint\n%s" % str(e)) return @@ -3694,6 +3703,9 @@ class FlatCAMExcEditor(QtCore.QObject): # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False) self.launched_from_shortcuts = False + # this var will store the state of the toolbar before starting the editor + self.toolbar_old_state = False + self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn) self.name_entry.returnPressed.connect(self.on_name_activate) self.addtool_btn.clicked.connect(self.on_tool_add) @@ -3704,6 +3716,17 @@ class FlatCAMExcEditor(QtCore.QObject): self.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio) + self.app.ui.exc_add_array_drill_menuitem.triggered.connect(self.exc_add_drill_array) + self.app.ui.exc_add_drill_menuitem.triggered.connect(self.exc_add_drill) + + self.app.ui.exc_resize_drill_menuitem.triggered.connect(self.exc_resize_drills) + self.app.ui.exc_copy_drill_menuitem.triggered.connect(self.exc_copy_drills) + self.app.ui.exc_delete_drill_menuitem.triggered.connect(self.on_delete_btn) + + self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills) + + + # Init GUI self.drill_array_size_entry.set_value(5) self.drill_pitch_entry.set_value(2.54) self.drill_angle_entry.set_value(12) @@ -4046,7 +4069,7 @@ class FlatCAMExcEditor(QtCore.QObject): # each time a tool diameter is edited or added self.olddia_newdia[tool_dia] = tool_dia else: - self.app.inform.emit("[warning_notcl]Tool already in the original or actual tool list.\n" + self.app.inform.emit("[WARNING_NOTCL]Tool already in the original or actual tool list.\n" "Save and reedit Excellon if you need to add this tool. ") return @@ -4084,7 +4107,7 @@ class FlatCAMExcEditor(QtCore.QObject): else: deleted_tool_dia_list.append(float('%.4f' % dia)) except: - self.app.inform.emit("[warning_notcl]Select a tool in Tool Table") + self.app.inform.emit("[WARNING_NOTCL]Select a tool in Tool Table") return for deleted_tool_dia in deleted_tool_dia_list: @@ -4172,8 +4195,26 @@ class FlatCAMExcEditor(QtCore.QObject): self.shapes.enabled = True self.tool_shape.enabled = True # self.app.app_cursor.enabled = True - self.app.ui.snap_max_dist_entry.setDisabled(False) + + self.app.ui.snap_max_dist_entry.setEnabled(True) self.app.ui.corner_snap_btn.setEnabled(True) + self.app.ui.snap_magnet.setVisible(True) + self.app.ui.corner_snap_btn.setVisible(True) + + self.app.ui.exc_editor_menu.setDisabled(False) + self.app.ui.exc_editor_menu.menuAction().setVisible(True) + + self.app.ui.update_obj_btn.setEnabled(True) + self.app.ui.e_editor_cmenu.setEnabled(True) + + self.app.ui.exc_edit_toolbar.setDisabled(False) + self.app.ui.exc_edit_toolbar.setVisible(True) + # self.app.ui.snap_toolbar.setDisabled(False) + + # start with GRID toolbar activated + if self.app.ui.grid_snap_btn.isChecked() is False: + self.app.ui.grid_snap_btn.trigger() + # Tell the App that the editor is active self.editor_active = True @@ -4181,9 +4222,35 @@ class FlatCAMExcEditor(QtCore.QObject): self.disconnect_canvas_event_handlers() self.clear() self.app.ui.exc_edit_toolbar.setDisabled(True) - self.app.ui.exc_edit_toolbar.setVisible(False) - self.app.ui.snap_max_dist_entry.setDisabled(True) - self.app.ui.corner_snap_btn.setEnabled(False) + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("theme"): + theme = settings.value('theme', type=str) + if theme == 'standard': + # self.app.ui.exc_edit_toolbar.setVisible(False) + + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + self.app.ui.snap_magnet.setVisible(False) + self.app.ui.corner_snap_btn.setVisible(False) + elif theme == 'compact': + # self.app.ui.exc_edit_toolbar.setVisible(True) + + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + self.app.ui.snap_magnet.setVisible(True) + self.app.ui.corner_snap_btn.setVisible(True) + else: + # self.app.ui.exc_edit_toolbar.setVisible(False) + + self.app.ui.snap_max_dist_entry.setEnabled(False) + self.app.ui.corner_snap_btn.setEnabled(False) + self.app.ui.snap_magnet.setVisible(False) + self.app.ui.corner_snap_btn.setVisible(False) + + # set the Editor Toolbar visibility to what was before entering in the Editor + self.app.ui.exc_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \ + else self.app.ui.exc_edit_toolbar.setVisible(True) # Disable visuals self.shapes.enabled = False @@ -4193,6 +4260,14 @@ class FlatCAMExcEditor(QtCore.QObject): # Tell the app that the editor is no longer active self.editor_active = False + self.app.ui.exc_editor_menu.setDisabled(True) + self.app.ui.exc_editor_menu.menuAction().setVisible(False) + + self.app.ui.update_obj_btn.setEnabled(False) + + self.app.ui.g_editor_cmenu.setEnabled(False) + self.app.ui.e_editor_cmenu.setEnabled(False) + # Show original geometry if self.exc_obj: self.exc_obj.visible = True @@ -4250,7 +4325,7 @@ class FlatCAMExcEditor(QtCore.QObject): # self.storage = FlatCAMExcEditor.make_storage() self.replot() - def edit_exc_obj(self, exc_obj): + def edit_fcexcellon(self, exc_obj): """ Imports the geometry from the given FlatCAM Excellon object into the editor. @@ -4298,15 +4373,8 @@ class FlatCAMExcEditor(QtCore.QObject): self.storage_dict[tool_dia] = storage_elem self.replot() - self.app.ui.exc_edit_toolbar.setDisabled(False) - self.app.ui.exc_edit_toolbar.setVisible(True) - self.app.ui.snap_toolbar.setDisabled(False) - # start with GRID toolbar activated - if self.app.ui.grid_snap_btn.isChecked() is False: - self.app.ui.grid_snap_btn.trigger() - - def update_exc_obj(self, exc_obj): + def update_fcexcellon(self, exc_obj): """ Create a new Excellon object that contain the edited content of the source Excellon object @@ -4411,6 +4479,21 @@ class FlatCAMExcEditor(QtCore.QObject): # Switch notebook to Selected page self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab) + def update_options(self, obj): + try: + if not obj.options: + obj.options = {} + obj.options['xmin'] = 0 + obj.options['ymin'] = 0 + obj.options['xmax'] = 0 + obj.options['ymax'] = 0 + return True + else: + return False + except AttributeError: + obj.options = {} + return True + def new_edited_excellon(self, outname): """ Creates a new Excellon object for the edited Excellon. Thread-safe. @@ -4430,14 +4513,15 @@ class FlatCAMExcEditor(QtCore.QObject): excellon_obj.drills = self.new_drills excellon_obj.tools = self.new_tools excellon_obj.slots = self.new_slots + excellon_obj.options['name'] = outname try: excellon_obj.create_geometry() except KeyError: self.app.inform.emit( - "[error_notcl] There are no Tools definitions in the file. Aborting Excellon creation.") + "[ERROR_NOTCL] There are no Tools definitions in the file. Aborting Excellon creation.") except: - msg = "[error] An internal error has ocurred. See shell.\n" + msg = "[ERROR] An internal error has ocurred. See shell.\n" msg += traceback.format_exc() app_obj.inform.emit(msg) raise @@ -4469,7 +4553,7 @@ class FlatCAMExcEditor(QtCore.QObject): # self.draw_app.select_tool('select') self.complete = True current_tool = 'select' - self.app.inform.emit("[warning_notcl]Cancelled. There is no Tool/Drill selected") + self.app.inform.emit("[WARNING_NOTCL]Cancelled. There is no Tool/Drill selected") # This is to make the group behave as radio group if current_tool in self.tools_exc: @@ -4813,7 +4897,7 @@ class FlatCAMExcEditor(QtCore.QObject): if event.key.name == 'Escape': # TODO: ...? # self.on_tool_select("select") - self.app.inform.emit("[warning_notcl]Cancelled.") + self.app.inform.emit("[WARNING_NOTCL]Cancelled.") self.delete_utility_geometry() @@ -4830,7 +4914,7 @@ class FlatCAMExcEditor(QtCore.QObject): self.delete_selected() self.replot() else: - self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to delete.") + self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to delete.") return if event.key == '1': @@ -4862,7 +4946,7 @@ class FlatCAMExcEditor(QtCore.QObject): self.on_tool_select('copy') self.active_tool.set_origin((self.snap_x, self.snap_y)) else: - self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to copy.") + self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to copy.") return # Add Drill Hole Tool @@ -4899,7 +4983,7 @@ class FlatCAMExcEditor(QtCore.QObject): self.on_tool_select('move') self.active_tool.set_origin((self.snap_x, self.snap_y)) else: - self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to move.") + self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to move.") return # Resize Tool @@ -4923,39 +5007,9 @@ class FlatCAMExcEditor(QtCore.QObject): # Show Shortcut list if event.key.name == '`': - self.on_shortcut_list() + self.app.on_shortcut_list() return - def on_shortcut_list(self): - msg = '''Shortcut list in Geometry Editor
-
-1: Zoom Fit
-2: Zoom Out
-3: Zoom In
-A: Add an 'Drill Array'
-C: Copy Drill Hole
-D: Add an Drill Hole
-G: Grid Snap On/Off
-K: Corner Snap On/Off
-M: Move Drill Hole
-
-R: Resize a 'Drill Hole'
-S: Select Tool Active
-
-~: Show Shortcut List
-
-Enter: Finish Current Action
-Escape: Abort Current Action
-Delete: Delete Drill Hole''' - - helpbox =QtWidgets.QMessageBox() - helpbox.setText(msg) - helpbox.setWindowTitle("Help") - helpbox.setWindowIcon(QtGui.QIcon('share/help.png')) - helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok) - helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok) - helpbox.exec_() - def on_canvas_key_release(self, event): self.key = None @@ -5197,10 +5251,18 @@ class FlatCAMExcEditor(QtCore.QObject): self.select_tool('add_array') return + def exc_resize_drills(self): + self.select_tool('resize') + return + def exc_copy_drills(self): self.select_tool('copy') return + def exc_move_drills(self): + self.select_tool('move') + return + def distance(pt1, pt2): return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 7cf40810..87436aad 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -7,7 +7,7 @@ ############################################################ from PyQt5 import QtGui, QtCore, QtWidgets -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QSettings from GUIElements import * import platform @@ -254,16 +254,22 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ### View ### self.menuview = self.menu.addMenu('&View') - self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots') + self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots\tALT+1') self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'), - 'Disable all plots') + 'Disable all plots\tALT+2') self.menuviewdisableother = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'), - 'Disable non-selected') + 'Disable non-selected\tALT+3') # Separator self.menuview.addSeparator() - self.menuview_zoom_fit = self.menuview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "&Zoom Fit\t1") - self.menuview_zoom_in = self.menuview.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In\t2") - self.menuview_zoom_out = self.menuview.addAction(QtGui.QIcon('share/zoom_out32.png'), "&Zoom Out\t3") + self.menuview_zoom_fit = self.menuview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "&Zoom Fit\tV") + self.menuview_zoom_in = self.menuview.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In\t-") + self.menuview_zoom_out = self.menuview.addAction(QtGui.QIcon('share/zoom_out32.png'), "&Zoom Out\t=") + + self.menuview.addSeparator() + self.menuview_toggle_fscreen = self.menuview.addAction( + QtGui.QIcon('share/fscreen32.png'), "&Toggle FullScreen\tALT+F10") + self.menuview_toggle_parea = self.menuview.addAction( + QtGui.QIcon('share/plot32.png'), "&Toggle Plot Area\tCTRL+F10") self.menuview.addSeparator() self.menuview_toggle_grid = self.menuview.addAction(QtGui.QIcon('share/grid32.png'), "&Toggle Grid\tG") @@ -271,11 +277,26 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuview_toggle_workspace = self.menuview.addAction(QtGui.QIcon('share/workspace24.png'), "Toggle Workspace\tSHIFT+W") + ### Tool ### + # self.menutool = self.menu.addMenu('&Tool') + self.menutool = QtWidgets.QMenu('&Tool') + self.menutoolaction = self.menu.addMenu(self.menutool) + self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line\tS') + + ### 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\tF1') + self.menuhelp.addSeparator() + self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'), 'Shortcuts List\t`') + self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/videohelp24.png'), 'See on YouTube\tF2') + ### FlatCAM Editor menu ### # self.editor_menu = QtWidgets.QMenu("Editor") # self.menu.addMenu(self.editor_menu) - self.geo_editor_menu = QtWidgets.QMenu("Geo Editor") + self.geo_editor_menu = QtWidgets.QMenu(">Geo Editor<") self.menu.addMenu(self.geo_editor_menu) # self.select_menuitem = self.menu.addAction(QtGui.QIcon('share/pointer16.png'), "Select 'Esc'") @@ -320,26 +341,32 @@ class FlatCAMGUI(QtWidgets.QMainWindow): QtGui.QIcon('share/corner32.png'), "Toggle Corner Snap\tK" ) - # self.exc_editor_menu = QtWidgets.QMenu("Excellon Editor") - # self.menu.addMenu(self.exc_editor_menu) + self.exc_editor_menu = QtWidgets.QMenu(">Excellon Editor<") + self.menu.addMenu(self.exc_editor_menu) + self.exc_add_array_drill_menuitem = self.exc_editor_menu.addAction( + QtGui.QIcon('share/rectangle32.png'), 'Add Drill Array\tA') + self.exc_add_drill_menuitem = self.exc_editor_menu.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill\tD') + self.exc_editor_menu.addSeparator() + + self.exc_resize_drill_menuitem = self.exc_editor_menu.addAction( + QtGui.QIcon('share/resize16.png'), 'Resize Drill(S)\tR' + ) + self.exc_copy_drill_menuitem = self.exc_editor_menu.addAction(QtGui.QIcon('share/copy32.png'), 'Copy\tC') + self.exc_delete_drill_menuitem = self.exc_editor_menu.addAction( + QtGui.QIcon('share/deleteshape32.png'), 'Delete\tDEL' + ) + self.exc_editor_menu.addSeparator() + + self.exc_move_drill_menuitem = self.exc_editor_menu.addAction( + QtGui.QIcon('share/move32.png'), 'Move Drill(s)\tM') + + self.geo_editor_menu.menuAction().setVisible(False) self.geo_editor_menu.setDisabled(True) - # self.exc_editor_menu.setDisabled(True) - ### Tool ### - # self.menutool = self.menu.addMenu('&Tool') - self.menutool = QtWidgets.QMenu('&Tool') - self.menutoolaction = self.menu.addMenu(self.menutool) - self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line\tS') + self.exc_editor_menu.menuAction().setVisible(False) + self.exc_editor_menu.setDisabled(True) - ### 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\tF1') - self.menuhelp.addSeparator() - self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'), 'Shortcuts List\t`') - self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/videohelp24.png'), 'See on YouTube\tF2') ################################ ### Project Tab Context menu ### @@ -357,11 +384,67 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuproject.addSeparator() self.menuprojectproperties = self.menuproject.addAction(QtGui.QIcon('share/properties32.png'), 'Properties') + ################ + ### Splitter ### + ################ + + # IMPORTANT # + # The order: SPITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as + # desired. + self.splitter = QtWidgets.QSplitter() + self.setCentralWidget(self.splitter) + + # self.notebook = QtWidgets.QTabWidget() + self.notebook = FCDetachableTab(protect=True) + self.notebook.setTabsClosable(False) + self.notebook.useOldIndex(True) + + self.splitter.addWidget(self.notebook) + + self.splitter_left = QtWidgets.QSplitter(Qt.Vertical) + self.splitter.addWidget(self.splitter_left) + self.splitter_left.addWidget(self.notebook) + self.splitter_left.setHandleWidth(0) + ############### ### Toolbar ### ############### + + ### TOOLBAR INSTALLATION ### self.toolbarfile = QtWidgets.QToolBar('File Toolbar') + self.toolbarfile.setObjectName('File_TB') self.addToolBar(self.toolbarfile) + self.toolbargeo = QtWidgets.QToolBar('Edit Toolbar') + self.toolbargeo.setObjectName('Edit_TB') + self.addToolBar(self.toolbargeo) + self.toolbarview = QtWidgets.QToolBar('View Toolbar') + self.toolbarview.setObjectName('View_TB') + self.addToolBar(self.toolbarview) + self.toolbartools = QtWidgets.QToolBar('Tools Toolbar') + self.toolbartools.setObjectName('Tools_TB') + self.addToolBar(self.toolbartools) + self.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar') + self.exc_edit_toolbar.setObjectName('ExcEditor_TB') + self.addToolBar(self.exc_edit_toolbar) + self.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar') + self.geo_edit_toolbar.setObjectName('GeoEditor_TB') + self.addToolBar(self.geo_edit_toolbar) + + self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar') + self.snap_toolbar.setObjectName('Snap_TB') + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("theme"): + theme = settings.value('theme', type=str) + if theme == 'standard': + self.addToolBar(self.snap_toolbar) + elif theme == 'compact': + self.snap_toolbar.setMaximumHeight(30) + self.splitter_left.addWidget(self.snap_toolbar) + else: + self.addToolBar(self.snap_toolbar) + + ### File Toolbar ### self.file_open_gerber_btn = self.toolbarfile.addAction(QtGui.QIcon('share/flatcam_icon32.png'), "Open GERBER") self.file_open_excellon_btn = self.toolbarfile.addAction(QtGui.QIcon('share/drill32.png'), "Open EXCELLON") @@ -369,9 +452,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.file_open_btn = self.toolbarfile.addAction(QtGui.QIcon('share/folder32.png'), "Open project") self.file_save_btn = self.toolbarfile.addAction(QtGui.QIcon('share/floppy32.png'), "Save project") - self.toolbargeo = QtWidgets.QToolBar('Edit Toolbar') - self.addToolBar(self.toolbargeo) - + ### Edit Toolbar ### self.newgeo_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_geo32_bis.png'), "New Blank Geometry") self.newexc_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_exc32.png'), "New Blank Excellon") self.toolbargeo.addSeparator() @@ -379,12 +460,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.update_obj_btn = self.toolbargeo.addAction( QtGui.QIcon('share/edit_ok32_bis.png'), "Save Object and close the Editor" ) - self.update_obj_btn.setEnabled(False) + self.toolbargeo.addSeparator() self.delete_btn = self.toolbargeo.addAction(QtGui.QIcon('share/cancel_edit32.png'), "&Delete") - self.toolbarview = QtWidgets.QToolBar('View Toolbar') - self.addToolBar(self.toolbarview) + ### View Toolbar ### self.replot_btn = self.toolbarview.addAction(QtGui.QIcon('share/replot32.png'), "&Replot") self.clear_plot_btn = self.toolbarview.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear plot") self.zoom_in_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_in32.png'), "Zoom In") @@ -393,14 +473,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # self.toolbarview.setVisible(False) - self.toolbartools = QtWidgets.QToolBar('Tools Toolbar') - self.addToolBar(self.toolbartools) + ### Tools Toolbar ### self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") ### Drill Editor Toolbar ### - self.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar') - self.addToolBar(self.exc_edit_toolbar) - self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole') self.add_drill_array_btn = self.exc_edit_toolbar.addAction( @@ -414,18 +490,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.exc_edit_toolbar.addSeparator() self.move_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Drill") - self.exc_edit_toolbar.setDisabled(True) - self.exc_edit_toolbar.setVisible(False) - ### Geometry Editor Toolbar ### - self.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar') - self.geo_edit_toolbar.setVisible(False) - self.addToolBar(self.geo_edit_toolbar) - self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') - self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle') + self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), + 'Add Rectangle') self.geo_edit_toolbar.addSeparator() self.geo_add_path_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path') @@ -438,22 +508,23 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_edit_toolbar.addSeparator() self.geo_union_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union') self.geo_intersection_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/intersection32.png'), - 'Polygon Intersection') - self.geo_subtract_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction') + 'Polygon Intersection') + self.geo_subtract_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), + 'Polygon Subtraction') self.geo_edit_toolbar.addSeparator() self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'") - self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), + "Delete Shape '-'") self.geo_edit_toolbar.addSeparator() self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") ### Snap Toolbar ### - self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar') # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID - self.addToolBar(self.snap_toolbar) + # self.addToolBar(self.snap_toolbar) self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid') self.grid_gap_x_entry = FCEntry2() @@ -477,25 +548,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/corner32.png'), 'Snap to corner') - self.snap_max_dist_entry = QtWidgets.QLineEdit() + 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_magnet = self.snap_toolbar.addWidget(self.snap_max_dist_entry) - self.grid_snap_btn.setCheckable(True) - self.corner_snap_btn.setCheckable(True) - - ################ - ### Splitter ### - ################ - self.splitter = QtWidgets.QSplitter() - self.setCentralWidget(self.splitter) ################ ### Notebook ### ################ - self.notebook = QtWidgets.QTabWidget() - self.splitter.addWidget(self.notebook) ### Project ### self.project_tab = QtWidgets.QWidget() @@ -527,16 +588,20 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.right_lay = QtWidgets.QVBoxLayout() self.right_lay.setContentsMargins(0, 0, 0, 0) self.right_widget.setLayout(self.right_lay) - self.plot_tab_area = FCTab() + # self.plot_tab_area = FCTab() + self.plot_tab_area = FCDetachableTab(protect=False, protect_by_name=['Plot Area']) + self.plot_tab_area.useOldIndex(True) + self.right_lay.addWidget(self.plot_tab_area) self.plot_tab_area.setTabsClosable(True) - plot_tab = QtWidgets.QWidget() - self.plot_tab_area.addTab(plot_tab, "Plot Area") + self.plot_tab = QtWidgets.QWidget() + self.plot_tab.setObjectName("plotarea") + self.plot_tab_area.addTab(self.plot_tab, "Plot Area") self.right_layout = QtWidgets.QVBoxLayout() self.right_layout.setContentsMargins(2, 2, 2, 2) - plot_tab.setLayout(self.right_layout) + self.plot_tab.setLayout(self.right_layout) # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON self.plot_tab_area.protectTab(0) @@ -666,6 +731,424 @@ class FlatCAMGUI(QtWidgets.QMainWindow): "which is the file storing the working default preferences.") self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button) + ######################################## + ### HERE WE BUILD THE SHORTCUTS LIST. TAB AREA ### + ######################################## + self.shortcuts_tab = QtWidgets.QWidget() + self.sh_tab_layout = QtWidgets.QVBoxLayout() + self.sh_tab_layout.setContentsMargins(2, 2, 2, 2) + self.shortcuts_tab.setLayout(self.sh_tab_layout) + + self.sh_hlay = QtWidgets.QHBoxLayout() + self.sh_title = QtWidgets.QTextEdit( + 'Shortcut Key List') + self.sh_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.sh_title.setFrameStyle(QtWidgets.QFrame.NoFrame) + self.sh_title.setMaximumHeight(30) + font = self.sh_title.font() + font.setPointSize(12) + self.sh_title.setFont(font) + + self.sh_tab_layout.addWidget(self.sh_title) + self.sh_tab_layout.addLayout(self.sh_hlay) + + self.app_sh_msg = '''General Shortcut list
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
~ SHOW SHORTCUT LIST
  
1 Switch to Project Tab
2 Switch to Selected Tab
3 Switch to Tool Tab
  
E Edit Object (if selected)
G Grid On/Off
J Jump to Coordinates
L New Excellon
M Move Obj
N New Geometry
O Set Origin
Q Change Units
P Open Properties Tool
R Rotate by 90 degree CW
S Shell Toggle
V Zoom Fit
X Flip on X_axis
Y Flip on Y_axis
'=' Zoom Out
'-' Zoom In
  
CTRL+A Select All
CTRL+C Copy Obj
CTRL+E Open Excellon File
CTRL+G Open Gerber File
CTRL+N New Project
CTRL+M Measurement Tool
CTRL+O Open Project
CTRL+S Save Project As
CTRL+F10 Toggle Plot Area
  
SHIFT+C Copy Obj_Name
SHIFT+G Toggle the axis
SHIFT+P Open Preferences Window
SHIFT+R Rotate by 90 degree CCW
SHIFT+S Run a Script
SHIFT+W Toggle the workspace
SHIFT+X Skew on X axis
SHIFT+Y Skew on Y axis
  
ALT+C Calculators Tool
ALT+D 2-Sided PCB Tool
ALT+L Film PCB Tool
ALT+N Non-Copper Clearing Tool
ALT+P Paint Area Tool
ALT+R Transformation Tool
ALT+U Cutout PCB Tool
ALT+1 Enable all Plots
ALT+2 Disable all Plots
ALT+3 Disable Non-selected Plots
ALT+F10 Toggle Full Screen
  
F1 Open Online Manual
F2 Open Online Tutorials
Del Delete Obj
SPACE En(Dis)able Obj Plot
+ +''' + + self.sh_app = QtWidgets.QTextEdit() + self.sh_app.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + self.sh_app.setText(self.app_sh_msg) + self.sh_app.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sh_hlay.addWidget(self.sh_app) + + self.editor_sh_msg = '''Editor Shortcut list
+
+GEOMETRY EDITOR
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
A Draw an Arc
B Buffer Tool
C Copy Geo Item
E Polygon Intersection Tool
I Paint Tool
K Toggle Corner Snap
M Move Geo Item
N Draw a Polygon
O Draw a Circle
P Draw a Path
R Draw Rectangle
S Polygon Substraction Tool
T Add Text Tool
U Polygon Union Tool
X Polygon Cut Tool
  
CTRL+S Save Object and Exit Editor
  
Space Rotate Geometry
ENTER Finish drawing for certain tools
ESC Abort and return to Select
Del Delete Shape
+
+
+EXCELLON EDITOR
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
A Add Drill Array
C Copy Drill(s)
D Add Drill
M Move Drill(s)
R Resize Drill(s)
  
Del Delete Drill(s)
  
ESC Abort and return to Select
CTRL+S Save Object and Exit Editor
+ ''' + self.sh_editor = QtWidgets.QTextEdit() + self.sh_editor.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.sh_editor.setText(self.editor_sh_msg) + self.sh_editor.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.sh_hlay.addWidget(self.sh_editor) + ############################################################## ### HERE WE BUILD THE CONTEXT MENU FOR RMB CLICK ON CANVAS ### @@ -823,6 +1306,215 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.filename = "" self.setAcceptDrops(True) + # # restore the Toolbar State from file + # try: + # with open(self.app.data_path + '\gui_state.config', 'rb') as stream: + # self.restoreState(QtCore.QByteArray(stream.read())) + # log.debug("FlatCAMGUI.__init__() --> UI state restored.") + # except IOError: + # log.debug("FlatCAMGUI.__init__() --> UI state not restored. IOError") + # pass + + ###################### + ### INITIALIZE GUI ### + ###################### + + self.grid_snap_btn.setCheckable(True) + self.corner_snap_btn.setCheckable(True) + self.update_obj_btn.setEnabled(False) + # start with GRID activated + self.grid_snap_btn.trigger() + + self.g_editor_cmenu.setEnabled(False) + self.e_editor_cmenu.setEnabled(False) + + # restore the Toolbar State from file + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("saved_gui_state"): + saved_gui_state = settings.value('saved_gui_state') + self.restoreState(saved_gui_state) + log.debug("FlatCAMGUI.__init__() --> UI state restored.") + + if settings.contains("theme"): + theme = settings.value('theme', type=str) + if theme == 'standard': + self.exc_edit_toolbar.setVisible(False) + self.exc_edit_toolbar.setDisabled(True) + self.geo_edit_toolbar.setVisible(False) + self.geo_edit_toolbar.setDisabled(True) + + self.corner_snap_btn.setVisible(False) + self.snap_magnet.setVisible(False) + elif theme == 'compact': + self.exc_edit_toolbar.setDisabled(True) + self.geo_edit_toolbar.setDisabled(True) + self.snap_magnet.setVisible(True) + self.corner_snap_btn.setVisible(True) + self.snap_magnet.setDisabled(True) + self.corner_snap_btn.setDisabled(True) + else: + self.exc_edit_toolbar.setVisible(False) + self.exc_edit_toolbar.setDisabled(True) + self.geo_edit_toolbar.setVisible(False) + self.geo_edit_toolbar.setDisabled(True) + + self.corner_snap_btn.setVisible(False) + self.snap_magnet.setVisible(False) + + def populate_toolbars(self): + + ### File Toolbar ### + self.file_open_gerber_btn = self.toolbarfile.addAction(QtGui.QIcon('share/flatcam_icon32.png'), + "Open GERBER") + self.file_open_excellon_btn = self.toolbarfile.addAction(QtGui.QIcon('share/drill32.png'), "Open EXCELLON") + self.toolbarfile.addSeparator() + self.file_open_btn = self.toolbarfile.addAction(QtGui.QIcon('share/folder32.png'), "Open project") + self.file_save_btn = self.toolbarfile.addAction(QtGui.QIcon('share/floppy32.png'), "Save project") + + ### Edit Toolbar ### + self.newgeo_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_geo32_bis.png'), "New Blank Geometry") + self.newexc_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_exc32.png'), "New Blank Excellon") + self.toolbargeo.addSeparator() + self.editgeo_btn = self.toolbargeo.addAction(QtGui.QIcon('share/edit32.png'), "Editor") + self.update_obj_btn = self.toolbargeo.addAction( + QtGui.QIcon('share/edit_ok32_bis.png'), "Save Object and close the Editor" + ) + + self.toolbargeo.addSeparator() + self.delete_btn = self.toolbargeo.addAction(QtGui.QIcon('share/cancel_edit32.png'), "&Delete") + + ### View Toolbar ### + self.replot_btn = self.toolbarview.addAction(QtGui.QIcon('share/replot32.png'), "&Replot") + self.clear_plot_btn = self.toolbarview.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear plot") + self.zoom_in_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_in32.png'), "Zoom In") + self.zoom_out_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_out32.png'), "Zoom Out") + self.zoom_fit_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "Zoom Fit") + + # self.toolbarview.setVisible(False) + + ### Tools Toolbar ### + self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") + + ### Drill Editor Toolbar ### + self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole') + self.add_drill_array_btn = self.exc_edit_toolbar.addAction( + QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array') + self.resize_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/resize16.png'), 'Resize Drill') + self.exc_edit_toolbar.addSeparator() + + self.copy_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Drill') + self.delete_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Drill") + + self.exc_edit_toolbar.addSeparator() + self.move_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Drill") + + ### Geometry Editor Toolbar ### + self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') + self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') + self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle') + + self.geo_edit_toolbar.addSeparator() + self.geo_add_path_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path') + self.geo_add_polygon_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon') + self.geo_edit_toolbar.addSeparator() + self.geo_add_text_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/text32.png'), 'Add Text') + self.geo_add_buffer_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), 'Add Buffer') + self.geo_add_paint_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/paint20_1.png'), 'Paint Shape') + + self.geo_edit_toolbar.addSeparator() + self.geo_union_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union') + self.geo_intersection_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/intersection32.png'), + 'Polygon Intersection') + self.geo_subtract_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction') + + self.geo_edit_toolbar.addSeparator() + self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') + self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") + self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") + + self.geo_edit_toolbar.addSeparator() + self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") + + ### Snap Toolbar ### + # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID + # self.addToolBar(self.snap_toolbar) + + self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid') + self.grid_gap_x_entry = FCEntry2() + 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 = FCEntry2() + self.grid_gap_y_entry.setMaximumWidth(70) + self.grid_gap_y_entry.setToolTip("Grid Y distance") + self.snap_toolbar.addWidget(self.grid_gap_y_entry) + + self.grid_space_label = QtWidgets.QLabel(" ") + self.snap_toolbar.addWidget(self.grid_space_label) + self.grid_gap_link_cb = FCCheckBox() + self.grid_gap_link_cb.setToolTip("When active, value on Grid_X\n" + "is copied to the Grid_Y value.") + self.snap_toolbar.addWidget(self.grid_gap_link_cb) + + self.ois_grid = OptionalInputSection(self.grid_gap_link_cb, [self.grid_gap_y_entry], logic=False) + + 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_magnet = self.snap_toolbar.addWidget(self.snap_max_dist_entry) + + self.grid_snap_btn.setCheckable(True) + self.corner_snap_btn.setCheckable(True) + self.update_obj_btn.setEnabled(False) + # start with GRID activated + self.grid_snap_btn.trigger() + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("theme"): + theme = settings.value('theme', type=str) + if theme == 'standard': + self.exc_edit_toolbar.setVisible(False) + self.exc_edit_toolbar.setDisabled(True) + self.geo_edit_toolbar.setVisible(False) + self.geo_edit_toolbar.setDisabled(True) + + self.corner_snap_btn.setVisible(False) + self.snap_magnet.setVisible(False) + elif theme == 'compact': + self.exc_edit_toolbar.setVisible(True) + self.exc_edit_toolbar.setDisabled(True) + self.geo_edit_toolbar.setVisible(True) + self.geo_edit_toolbar.setDisabled(True) + + self.corner_snap_btn.setVisible(True) + self.snap_magnet.setVisible(True) + self.corner_snap_btn.setDisabled(True) + self.snap_magnet.setDisabled(True) + + def keyPressEvent(self, event): + + if event.key() == QtCore.Qt.Key_1: + self.app.on_select_tab('project') + + if event.key() == QtCore.Qt.Key_2: + self.app.on_select_tab('selected') + + if event.key() == QtCore.Qt.Key_3: + self.app.on_select_tab('tool') + + # Show shortcut list + if event.key() == QtCore.Qt.Key_Ampersand: + self.app.on_shortcut_list() + + if event.key() == QtCore.Qt.Key_QuoteLeft: + self.app.on_shortcut_list() + def dragEnterEvent(self, event): if event.mimeData().hasUrls: event.accept() @@ -889,6 +1581,19 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.final_save.emit() if self.app.should_we_quit is True: + # # save toolbar state to file + # with open(self.app.data_path + '\gui_state.config', 'wb') as stream: + # stream.write(self.saveState().data()) + # log.debug("FlatCAMGUI.__init__() --> UI state saved.") + # QtWidgets.qApp.quit() + + # save toolbar state to file + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('saved_gui_state', self.saveState()) + + # This will write the setting to the platform specific storage. + del settings + log.debug("FlatCAMGUI.__init__() --> UI state saved.") QtWidgets.qApp.quit() else: self.app.should_we_quit = True @@ -902,10 +1607,10 @@ class GeneralPreferencesUI(QtWidgets.QWidget): self.setLayout(self.layout) self.general_app_group = GeneralAppPrefGroupUI() - self.general_app_group.setFixedWidth(260) + self.general_app_group.setFixedWidth(250) self.general_gui_group = GeneralGUIPrefGroupUI() - self.general_gui_group.setFixedWidth(260) + self.general_gui_group.setFixedWidth(250) self.layout.addWidget(self.general_app_group) self.layout.addWidget(self.general_gui_group) @@ -920,9 +1625,9 @@ class GerberPreferencesUI(QtWidgets.QWidget): self.setLayout(self.layout) self.gerber_gen_group = GerberGenPrefGroupUI() - self.gerber_gen_group.setFixedWidth(260) + self.gerber_gen_group.setFixedWidth(250) self.gerber_opt_group = GerberOptPrefGroupUI() - self.gerber_opt_group.setFixedWidth(260) + self.gerber_opt_group.setFixedWidth(250) self.layout.addWidget(self.gerber_gen_group) self.layout.addWidget(self.gerber_opt_group) @@ -937,9 +1642,9 @@ class ExcellonPreferencesUI(QtWidgets.QWidget): self.setLayout(self.layout) self.excellon_gen_group = ExcellonGenPrefGroupUI() - self.excellon_gen_group.setFixedWidth(260) + self.excellon_gen_group.setFixedWidth(275) self.excellon_opt_group = ExcellonOptPrefGroupUI() - self.excellon_opt_group.setFixedWidth(260) + self.excellon_opt_group.setFixedWidth(275) self.layout.addWidget(self.excellon_gen_group) self.layout.addWidget(self.excellon_opt_group) @@ -954,9 +1659,9 @@ class GeometryPreferencesUI(QtWidgets.QWidget): self.setLayout(self.layout) self.geometry_gen_group = GeometryGenPrefGroupUI() - self.geometry_gen_group.setFixedWidth(260) + self.geometry_gen_group.setFixedWidth(275) self.geometry_opt_group = GeometryOptPrefGroupUI() - self.geometry_opt_group.setFixedWidth(260) + self.geometry_opt_group.setFixedWidth(275) self.layout.addWidget(self.geometry_gen_group) self.layout.addWidget(self.geometry_opt_group) @@ -971,15 +1676,21 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.setLayout(self.layout) self.tools_ncc_group = ToolsNCCPrefGroupUI() - self.tools_ncc_group.setFixedWidth(260) + self.tools_ncc_group.setFixedWidth(200) self.tools_paint_group = ToolsPaintPrefGroupUI() - self.tools_paint_group.setFixedWidth(260) + self.tools_paint_group.setFixedWidth(200) self.tools_cutout_group = ToolsCutoutPrefGroupUI() - self.tools_cutout_group.setFixedWidth(260) + self.tools_cutout_group.setFixedWidth(200) self.tools_2sided_group = Tools2sidedPrefGroupUI() - self.tools_2sided_group.setFixedWidth(260) + self.tools_2sided_group.setFixedWidth(200) + + self.tools_film_group = ToolsFilmPrefGroupUI() + self.tools_film_group.setFixedWidth(200) + + self.tools_panelize_group = ToolsPanelizePrefGroupUI() + self.tools_panelize_group.setFixedWidth(200) self.vlay = QtWidgets.QVBoxLayout() self.vlay.addWidget(self.tools_ncc_group) @@ -988,9 +1699,14 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.vlay1 = QtWidgets.QVBoxLayout() self.vlay1.addWidget(self.tools_cutout_group) self.vlay1.addWidget(self.tools_2sided_group) + self.vlay1.addWidget(self.tools_film_group) + + self.vlay2 = QtWidgets.QVBoxLayout() + self.vlay2.addWidget(self.tools_panelize_group) self.layout.addLayout(self.vlay) self.layout.addLayout(self.vlay1) + self.layout.addLayout(self.vlay2) self.layout.addStretch() @@ -1050,6 +1766,11 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): ) self.gridy_entry = LengthEntry() + # Snap Max Entry + self.snap_max_label = QtWidgets.QLabel('Snap Max:') + self.snap_max_label.setToolTip("Max. magnet distance") + self.snap_max_dist_entry = FCEntry() + # Workspace self.workspace_lbl = QtWidgets.QLabel('Workspace:') self.workspace_lbl.setToolTip( @@ -1247,6 +1968,16 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box_child_11.addWidget(self.sel_draw_color_button) self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + # Theme selection + self.theme_label = QtWidgets.QLabel('Theme:') + self.alt_sf_color_label.setToolTip( + "Select a theme for FlatCAM." + ) + self.theme_combo = FCComboBox() + self.theme_combo.addItem("Standard") + self.theme_combo.addItem("Compact") + self.theme_combo.setCurrentIndex(0) + # Just to add empty rows self.spacelabel = QtWidgets.QLabel('') @@ -1256,6 +1987,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box.addRow(self.gridx_label, self.gridx_entry) self.form_box.addRow(self.gridy_label, self.gridy_entry) + self.form_box.addRow(self.snap_max_label, self.snap_max_dist_entry) self.form_box.addRow(self.workspace_lbl, self.workspace_cb) self.form_box.addRow(self.workspace_type_lbl, self.wk_cb) @@ -1272,6 +2004,8 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box.addRow(self.draw_color_label, self.form_box_child_10) self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11) + self.form_box.addRow(self.spacelabel, self.spacelabel) + self.form_box.addRow(self.theme_label, self.theme_combo) # Add the QFormLayout that holds the Application general defaults # to the main layout of this TAB self.layout.addLayout(self.form_box) @@ -1781,7 +2515,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI): self.optimization_time_label.setToolTip( "When OR-Tools Metaheuristic (MH) is enabled there is a\n" "maximum threshold for how much time is spent doing the\n" - "path optimization. This max duration is set here." + "path optimization. This max duration is set here.\n" + "In seconds." ) @@ -1952,6 +2687,36 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI): self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus) grid2.addWidget(self.pp_excellon_name_cb, 12, 1) + # Probe depth + self.pdepth_label = QtWidgets.QLabel("Probe Z depth:") + self.pdepth_label.setToolTip( + "The maximum depth that the probe is allowed\n" + "to probe. Negative value, in current units." + ) + grid2.addWidget(self.pdepth_label, 13, 0) + self.pdepth_entry = FCEntry() + grid2.addWidget(self.pdepth_entry, 13, 1) + + # Probe feedrate + self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:") + self.feedrate_probe_label.setToolTip( + "The feedrate used while the probe is probing." + ) + grid2.addWidget(self.feedrate_probe_label, 14, 0) + self.feedrate_probe_entry = FCEntry() + grid2.addWidget(self.feedrate_probe_entry, 14, 1) + + fplungelabel = QtWidgets.QLabel('Fast Plunge:') + fplungelabel.setToolTip( + "By checking this, the vertical move from\n" + "Z_Toolchange to Z_move is done with G0,\n" + "meaning the fastest speed available.\n" + "WARNING: the move is done at Toolchange X,Y coords." + ) + self.fplunge_cb = FCCheckBox() + grid2.addWidget(fplungelabel, 15, 0) + grid2.addWidget(self.fplunge_cb, 15, 1) + #### Choose what to use for Gcode creation: Drills, Slots or Both excellon_gcode_type_label = QtWidgets.QLabel('Gcode: ') excellon_gcode_type_label.setToolTip( @@ -1963,8 +2728,8 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI): self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'}, {'label': 'Slots', 'value': 'slots'}, {'label': 'Both', 'value': 'both'}]) - grid2.addWidget(excellon_gcode_type_label, 13, 0) - grid2.addWidget(self.excellon_gcode_type_radio, 13, 1) + grid2.addWidget(excellon_gcode_type_label, 16, 0) + grid2.addWidget(self.excellon_gcode_type_radio, 16, 1) # until I decide to implement this feature those remain disabled excellon_gcode_type_label.setDisabled(True) @@ -2248,6 +3013,37 @@ class GeometryOptPrefGroupUI(OptionsGroupUI): self.pp_geometry_name_cb.setFocusPolicy(Qt.StrongFocus) grid1.addWidget(self.pp_geometry_name_cb, 16, 1) + # Probe depth + self.pdepth_label = QtWidgets.QLabel("Probe Z depth:") + self.pdepth_label.setToolTip( + "The maximum depth that the probe is allowed\n" + "to probe. Negative value, in current units." + ) + grid1.addWidget(self.pdepth_label, 17, 0) + self.pdepth_entry = FCEntry() + grid1.addWidget(self.pdepth_entry, 17, 1) + + # Probe feedrate + self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:") + self.feedrate_probe_label.setToolTip( + "The feedrate used while the probe is probing." + ) + grid1.addWidget(self.feedrate_probe_label, 18, 0) + self.feedrate_probe_entry = FCEntry() + grid1.addWidget(self.feedrate_probe_entry, 18, 1) + + # Fast Move from Z Toolchange + fplungelabel = QtWidgets.QLabel('Fast Plunge:') + fplungelabel.setToolTip( + "By checking this, the vertical move from\n" + "Z_Toolchange to Z_move is done with G0,\n" + "meaning the fastest speed available.\n" + "WARNING: the move is done at Toolchange X,Y coords." + ) + self.fplunge_cb = FCCheckBox() + grid1.addWidget(fplungelabel, 19, 0) + grid1.addWidget(self.fplunge_cb, 19, 1) + # Size of trace segment on X axis segx_label = QtWidgets.QLabel("Seg. X size:") segx_label.setToolTip( @@ -2255,9 +3051,9 @@ class GeometryOptPrefGroupUI(OptionsGroupUI): "Useful for auto-leveling.\n" "A value of 0 means no segmentation on the X axis." ) - grid1.addWidget(segx_label, 17, 0) + grid1.addWidget(segx_label, 20, 0) self.segx_entry = FCEntry() - grid1.addWidget(self.segx_entry, 17, 1) + grid1.addWidget(self.segx_entry, 20, 1) # Size of trace segment on Y axis segy_label = QtWidgets.QLabel("Seg. Y size:") @@ -2266,9 +3062,9 @@ class GeometryOptPrefGroupUI(OptionsGroupUI): "Useful for auto-leveling.\n" "A value of 0 means no segmentation on the Y axis." ) - grid1.addWidget(segy_label, 18, 0) + grid1.addWidget(segy_label, 21, 0) self.segy_entry = FCEntry() - grid1.addWidget(self.segy_entry, 18, 1) + grid1.addWidget(self.segy_entry, 22, 1) self.layout.addStretch() @@ -2387,7 +3183,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): self.setTitle(str("NCC Tool Options")) ## Clear non-copper regions - self.clearcopper_label = QtWidgets.QLabel("Clear non-copper:") + self.clearcopper_label = QtWidgets.QLabel("Parameters:") self.clearcopper_label.setToolTip( "Create a Geometry object with\n" "toolpaths to cut all non-copper regions." @@ -2488,7 +3284,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI): self.setTitle(str("Cutout Tool Options")) ## Board cuttout - self.board_cutout_label = QtWidgets.QLabel("Board cutout:") + self.board_cutout_label = QtWidgets.QLabel("Parameters:") self.board_cutout_label.setToolTip( "Create toolpaths to cut around\n" "the PCB and separate it from\n" @@ -2571,7 +3367,7 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI): self.setTitle(str("2Sided Tool Options")) ## Board cuttout - self.dblsided_label = QtWidgets.QLabel("Double Sided:") + self.dblsided_label = QtWidgets.QLabel("Parameters:") self.dblsided_label.setToolTip( "A tool to help in creating a double sided\n" "PCB using alignment holes." @@ -2625,12 +3421,12 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): # OptionsGroupUI.__init__(self, "Paint Area Tool Options", parent=parent) super(ToolsPaintPrefGroupUI, self).__init__(self) - self.setTitle(str("Paint Area Tool Options")) + self.setTitle(str("Paint Tool Options")) # ------------------------------ ## Paint area # ------------------------------ - self.paint_label = QtWidgets.QLabel('Paint Area:') + self.paint_label = QtWidgets.QLabel('Parameters:') self.paint_label.setToolTip( "Creates tool paths to cover the\n" "whole area of a polygon (remove\n" @@ -2725,6 +3521,155 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): self.layout.addStretch() +class ToolsFilmPrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + # OptionsGroupUI.__init__(self, "Cutout Tool Options", parent=parent) + super(ToolsFilmPrefGroupUI, self).__init__(self) + + self.setTitle(str("Film Tool Options")) + + ## Board cuttout + self.film_label = QtWidgets.QLabel("Parameters:") + self.film_label.setToolTip( + "Create a PCB film from a Gerber or Geometry\n" + "FlatCAM object.\n" + "The file is saved in SVG format." + ) + self.layout.addWidget(self.film_label) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'}, {'label': 'Neg', 'value': 'neg'}]) + ftypelbl = QtWidgets.QLabel('Film Type:') + ftypelbl.setToolTip( + "Generate a Positive black film or a Negative film.\n" + "Positive means that it will print the features\n" + "with black on a white canvas.\n" + "Negative means that it will print the features\n" + "with white on a black canvas.\n" + "The Film format is SVG." + ) + grid0.addWidget(ftypelbl, 0, 0) + grid0.addWidget(self.film_type_radio, 0, 1) + + self.film_boundary_entry = FCEntry() + self.film_boundary_label = QtWidgets.QLabel("Border:") + self.film_boundary_label.setToolTip( + "Specify a border around the object.\n" + "Only for negative film.\n" + "It helps if we use as a Box Object the same \n" + "object as in Film Object. It will create a thick\n" + "black bar around the actual print allowing for a\n" + "better delimitation of the outline features which are of\n" + "white color like the rest and which may confound with the\n" + "surroundings if not for this border." + ) + grid0.addWidget(self.film_boundary_label, 1, 0) + grid0.addWidget(self.film_boundary_entry, 1, 1) + + self.film_scale_entry = FCEntry() + self.film_scale_label = QtWidgets.QLabel("Scale Stroke:") + self.film_scale_label.setToolTip( + "Scale the line stroke thickness of each feature in the SVG file.\n" + "It means that the line that envelope each SVG feature will be thicker or thinner,\n" + "therefore the fine features may be more affected by this parameter." + ) + grid0.addWidget(self.film_scale_label, 2, 0) + grid0.addWidget(self.film_scale_entry, 2, 1) + + self.layout.addStretch() + + +class ToolsPanelizePrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + # OptionsGroupUI.__init__(self, "Cutout Tool Options", parent=parent) + super(ToolsPanelizePrefGroupUI, self).__init__(self) + + self.setTitle(str("Panelize Tool Options")) + + ## Board cuttout + self.panelize_label = QtWidgets.QLabel("Parameters:") + self.panelize_label.setToolTip( + "Create an object that contains an array of (x, y) elements,\n" + "each element is a copy of the source object spaced\n" + "at a X distance, Y distance of each other." + ) + self.layout.addWidget(self.panelize_label) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + ## Spacing Columns + self.pspacing_columns = FCEntry() + self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:") + self.spacing_columns_label.setToolTip( + "Spacing between columns of the desired panel.\n" + "In current units." + ) + grid0.addWidget(self.spacing_columns_label, 0, 0) + grid0.addWidget(self.pspacing_columns, 0, 1) + + ## Spacing Rows + self.pspacing_rows = FCEntry() + self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:") + self.spacing_rows_label.setToolTip( + "Spacing between rows of the desired panel.\n" + "In current units." + ) + grid0.addWidget(self.spacing_rows_label, 1, 0) + grid0.addWidget(self.pspacing_rows, 1, 1) + + ## Columns + self.pcolumns = FCEntry() + self.columns_label = QtWidgets.QLabel("Columns:") + self.columns_label.setToolTip( + "Number of columns of the desired panel" + ) + grid0.addWidget(self.columns_label, 2, 0) + grid0.addWidget(self.pcolumns, 2, 1) + + ## Rows + self.prows = FCEntry() + self.rows_label = QtWidgets.QLabel("Rows:") + self.rows_label.setToolTip( + "Number of rows of the desired panel" + ) + grid0.addWidget(self.rows_label, 3, 0) + grid0.addWidget(self.prows, 3, 1) + + ## Constrains + self.pconstrain_cb = FCCheckBox("Constrain within:") + self.pconstrain_cb.setToolTip( + "Area define by DX and DY within to constrain the panel.\n" + "DX and DY values are in current units.\n" + "Regardless of how many columns and rows are desired,\n" + "the final panel will have as many columns and rows as\n" + "they fit completely within selected area." + ) + grid0.addWidget(self.pconstrain_cb, 4, 0) + + self.px_width_entry = FCEntry() + self.x_width_lbl = QtWidgets.QLabel("Width (DX):") + self.x_width_lbl.setToolTip( + "The width (DX) within which the panel must fit.\n" + "In current units." + ) + grid0.addWidget(self.x_width_lbl, 5, 0) + grid0.addWidget(self.px_width_entry, 5, 1) + + self.py_height_entry = FCEntry() + self.y_height_lbl = QtWidgets.QLabel("Height (DY):") + self.y_height_lbl.setToolTip( + "The height (DY)within which the panel must fit.\n" + "In current units." + ) + grid0.addWidget(self.y_height_lbl, 6, 0) + grid0.addWidget(self.py_height_entry, 6, 1) + + self.layout.addStretch() + + class FlatCAMActivityView(QtWidgets.QWidget): def __init__(self, parent=None): @@ -2789,11 +3734,11 @@ class FlatCAMInfoBar(QtWidgets.QWidget): def set_status(self, text, level="info"): level = str(level) self.pmap.fill() - if level == "error" or level == "error_notcl": + if level == "ERROR" or level == "ERROR_NOTCL": self.pmap = QtGui.QPixmap('share/redlight12.png') elif level == "success": self.pmap = QtGui.QPixmap('share/greenlight12.png') - elif level == "warning" or level == "warning_notcl": + elif level == "WARNING" or level == "WARNING_NOTCL": self.pmap = QtGui.QPixmap('share/yellowlight12.png') else: self.pmap = QtGui.QPixmap('share/graylight12.png') diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 8431132e..cd720a90 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -9,7 +9,7 @@ from io import StringIO from PyQt5 import QtCore, QtGui from PyQt5.QtCore import Qt -from copy import copy, deepcopy +import copy import inspect # TODO: For debugging only. from shapely.geometry.base import JOIN_STYLE from datetime import datetime @@ -165,7 +165,7 @@ class FlatCAMObj(QtCore.QObject): self.muted_ui = False def on_name_activate(self): - old_name = copy(self.options["name"]) + old_name = copy.copy(self.options["name"]) new_name = self.ui.name_entry.get_value() # update the SHELL auto-completer model data @@ -856,6 +856,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): "dwell": True, "dwelltime": 1000, "ppname_e": 'defaults', + "z_pdepth": -0.02, + "feedrate_probe": 3.0, "optimization_type": "R", "gcode_type": "drills" }) @@ -1238,6 +1240,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): "startz": self.ui.estartz_entry, "endz": self.ui.eendz_entry, "ppname_e": self.ui.pp_excellon_name_cb, + "z_pdepth": self.ui.pdepth_entry, + "feedrate_probe": self.ui.feedrate_probe_entry, "gcode_type": self.ui.excellon_gcode_type_radio }) @@ -1258,6 +1262,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click) self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click) + self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed) + def get_selected_tools_list(self): """ Returns the keys to the self.tools dictionary corresponding @@ -1467,12 +1473,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: - self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.") + self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") return False, "Error: No tools." for tool in tools: if tooldia > self.tools[tool]["C"]: - self.app.inform.emit("[error_notcl] Milling tool for DRILLS is larger than hole size. Cancelled.") + self.app.inform.emit("[ERROR_NOTCL] Milling tool for DRILLS is larger than hole size. Cancelled.") return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): @@ -1554,12 +1560,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: - self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.") + self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") return False, "Error: No tools." for tool in tools: if tooldia > self.tools[tool]["C"]: - self.app.inform.emit("[error_notcl] Milling tool for SLOTS is larger than hole size. Cancelled.") + self.app.inform.emit("[ERROR_NOTCL] Milling tool for SLOTS is larger than hole size. Cancelled.") return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): @@ -1627,6 +1633,22 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.generate_milling_slots(use_thread=False) + def on_pp_changed(self): + current_pp = self.ui.pp_excellon_name_cb.get_value() + + if "toolchange_probe" in current_pp.lower(): + self.ui.pdepth_entry.setVisible(True) + self.ui.pdepth_label.show() + + self.ui.feedrate_probe_entry.setVisible(True) + self.ui.feedrate_probe_label.show() + else: + self.ui.pdepth_entry.setVisible(False) + self.ui.pdepth_label.hide() + + self.ui.feedrate_probe_entry.setVisible(False) + self.ui.feedrate_probe_label.hide() + def on_create_cncjob_button_click(self, *args): self.app.report_usage("excellon_on_create_cncjob_button") self.read_form() @@ -1635,9 +1657,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): tools = self.get_selected_tools_list() if len(tools) == 0: - self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.") + self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") return + xmin = self.options['xmin'] + ymin = self.options['ymin'] + xmax = self.options['xmax'] + ymax = self.options['ymax'] + job_name = self.options["name"] + "_cnc" pp_excellon_name = self.options["ppname_e"] @@ -1655,6 +1682,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): job_obj.options['Tools_in_use'] = tool_table_items job_obj.options['type'] = 'Excellon' + job_obj.options['ppname_e'] = pp_excellon_name app_obj.progress.emit(20) job_obj.z_cut = self.options["drillz"] @@ -1665,10 +1693,37 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): job_obj.dwell = self.options["dwell"] job_obj.dwelltime = self.options["dwelltime"] job_obj.pp_excellon_name = pp_excellon_name - job_obj.toolchange_xy = "excellon" + job_obj.toolchange_xy = self.app.defaults["excellon_toolchangexy"] + job_obj.toolchange_xy_type = "excellon" job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"]) job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"]) + job_obj.options['xmin'] = xmin + job_obj.options['ymin'] = ymin + job_obj.options['xmax'] = xmax + job_obj.options['ymax'] = ymax + + try: + job_obj.z_pdepth = float(self.options["z_pdepth"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') + + try: + job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + 'or self.options["feedrate_probe"]') + # There could be more than one drill size... # job_obj.tooldia = # TODO: duplicate variable! # job_obj.options["tooldia"] = @@ -1678,7 +1733,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): drillz=self.options['drillz'], toolchange=self.options["toolchange"], toolchangez=self.options["toolchangez"], - toolchangexy=self.options["toolchangexy"], startz=self.options["startz"], endz=self.options["endz"], excellon_optimization_type=self.options["optimization_type"]) @@ -1724,10 +1778,17 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.options['feedrate_rapid'] *= factor self.options['toolchangez'] *= factor - coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")] - coords_xy[0] *= factor - coords_xy[1] *= factor - self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) + if self.app.defaults["excellon_toolchangexy"] == '': + self.options['toolchangexy'] = "0.0, 0.0" + else: + coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")] + if len(coords_xy) < 2: + self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be " + "in the format (x, y) \nbut now there is only one value, not two. ") + return 'fail' + coords_xy[0] *= factor + coords_xy[1] *= factor + self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) if self.options['startz'] is not None: self.options['startz'] *= factor @@ -1863,7 +1924,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon): for tool_uid in geo.tools: max_uid += 1 - geo_final.tools[max_uid] = dict(geo.tools[tool_uid]) + geo_final.tools[max_uid] = copy.deepcopy(geo.tools[tool_uid]) @staticmethod def get_pts(o): @@ -1931,6 +1992,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "toolchangexy": "0.0, 0.0", "startz": None, "ppname_g": 'default', + "z_pdepth": -0.02, + "feedrate_probe": 3.0, }) if "cnctooldia" not in self.options: @@ -2143,6 +2206,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "dwelltime": self.ui.dwelltime_entry, "multidepth": self.ui.mpass_cb, "ppname_g": self.ui.pp_geometry_name_cb, + "z_pdepth": self.ui.pdepth_entry, + "feedrate_probe": self.ui.feedrate_probe_entry, "depthperpass": self.ui.maxdepth_entry, "extracut": self.ui.extracut_cb, "toolchange": self.ui.toolchangeg_cb, @@ -2201,7 +2266,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): 'type': 'Rough', 'tool_type': 'C1', 'data': self.default_data, - 'solid_geometry': [] + 'solid_geometry': self.solid_geometry } }) else: @@ -2213,12 +2278,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): temp_tools = {} new_key = 0.0 for tooluid_key in self.tools: - val = dict(self.tools[tooluid_key]) - new_key = deepcopy(int(tooluid_key)) + val = copy.deepcopy(self.tools[tooluid_key]) + new_key = copy.deepcopy(int(tooluid_key)) temp_tools[new_key] = val self.tools.clear() - self.tools = dict(temp_tools) + self.tools = copy.deepcopy(temp_tools) self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() @@ -2274,7 +2339,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tool_uid: - tooluid_value['offset_value'] = self.ui.tool_offset_entry.get_value() + try: + tooluid_value['offset_value'] = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooluid_value['offset_value'] = float( + self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return def ui_connect(self): @@ -2391,13 +2467,26 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): last_data = None last_solid_geometry = [] + # if a Tool diameter entered is a char instead a number the final message of Tool adding is changed + # because the Default value for Tool is used. + change_message = False + if dia is not None: tooldia = dia else: - tooldia = self.ui.addtool_entry.get_value() + try: + tooldia = float(self.ui.addtool_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooldia = float(self.ui.addtool_entry.get_value().replace(',', '.')) + except ValueError: + change_message = True + tooldia = float(self.app.defaults["geometry_cnctooldia"]) + if tooldia is None: self.build_ui() - self.app.inform.emit("[error_notcl] Please enter the desired tool diameter in Float format.") + self.app.inform.emit("[ERROR_NOTCL] Please enter the desired tool diameter in Float format.") return # construct a list of all 'tooluid' in the self.tools @@ -2428,8 +2517,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): 'offset_value': 0.0, 'type': 'Rough', 'tool_type': 'C1', - 'data': dict(self.default_data), - 'solid_geometry': [] + 'data': copy.deepcopy(self.default_data), + 'solid_geometry': self.solid_geometry } }) else: @@ -2441,6 +2530,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): last_tool_type = self.tools[max_uid]['tool_type'] last_solid_geometry = self.tools[max_uid]['solid_geometry'] + # if previous geometry was empty (it may happen for the first tool added) + # then copy the object.solid_geometry + if not last_solid_geometry: + last_solid_geometry = self.solid_geometry + self.tools.update({ self.tooluid: { 'tooldia': tooldia, @@ -2448,8 +2542,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): 'offset_value': last_offset_value, 'type': last_type, 'tool_type': last_tool_type, - 'data': dict(last_data), - 'solid_geometry': deepcopy(last_solid_geometry) + 'data': copy.deepcopy(last_data), + 'solid_geometry': copy.deepcopy(last_solid_geometry) } }) # print("CURRENT", self.tools[-1]) @@ -2464,7 +2558,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): pass self.ser_attrs.append('tools') - self.app.inform.emit("[success] Tool added in Tool Table.") + if change_message is False: + self.app.inform.emit("[success] Tool added in Tool Table.") + else: + change_message = False + self.app.inform.emit("[ERROR_NOTCL]Default Tool added. Wrong value format entered.") self.build_ui() def on_tool_copy(self, all=None): @@ -2490,9 +2588,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) max_uid += 1 - self.tools[int(max_uid)] = dict(self.tools[tooluid_copy]) + self.tools[int(max_uid)] = copy.deepcopy(self.tools[tooluid_copy]) except AttributeError: - self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.") + self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.") self.build_ui() return except Exception as e: @@ -2500,16 +2598,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # deselect the table # self.ui.geo_tools_table.clearSelection() else: - self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.") + self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.") self.build_ui() return else: # we copy all tools in geo_tools_table try: - temp_tools = dict(self.tools) + temp_tools = copy.deepcopy(self.tools) max_uid += 1 for tooluid in temp_tools: - self.tools[int(max_uid)] = dict(temp_tools[tooluid]) + self.tools[int(max_uid)] = copy.deepcopy(temp_tools[tooluid]) temp_tools.clear() except Exception as e: log.debug("on_tool_copy() --> " + str(e)) @@ -2534,7 +2632,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui_disconnect() current_row = current_item.row() - tool_dia = float('%.4f' % float(self.ui.geo_tools_table.item(current_row, 1).text())) + try: + d = float(self.ui.geo_tools_table.item(current_row, 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + tool_dia = float('%.4f' % d) tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text()) self.tools[tooluid]['tooldia'] = tool_dia @@ -2563,14 +2672,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) - temp_tools = dict(self.tools) + temp_tools = copy.deepcopy(self.tools) for tooluid_key in self.tools: if int(tooluid_key) == tooluid_del: temp_tools.pop(tooluid_del, None) - self.tools = dict(temp_tools) + self.tools = copy.deepcopy(temp_tools) temp_tools.clear() except AttributeError: - self.app.inform.emit("[warning_notcl]Failed. Select a tool to delete.") + self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to delete.") self.build_ui() return except Exception as e: @@ -2578,7 +2687,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # deselect the table # self.ui.geo_tools_table.clearSelection() else: - self.app.inform.emit("[warning_notcl]Failed. Select a tool to delete.") + self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to delete.") self.build_ui() return else: @@ -2801,7 +2910,17 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText() tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text()) - offset_value_item = self.ui.tool_offset_entry.get_value() + try: + offset_value_item = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + offset_value_item = float(self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return # this new dict will hold the actual useful data, another dict that is the value of key 'data' temp_tools = {} @@ -2833,19 +2952,19 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # updated from self.app.defaults if data_key not in self.form_fields: temp_data[data_key] = value[data_key] - temp_dia[key] = dict(temp_data) + temp_dia[key] = copy.deepcopy(temp_data) temp_data.clear() if key == 'solid_geometry': - temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry']) + temp_dia[key] = copy.deepcopy(self.tools[tooluid_key]['solid_geometry']) - temp_tools[tooluid_key] = dict(temp_dia) + temp_tools[tooluid_key] = copy.deepcopy(temp_dia) else: - temp_tools[tooluid_key] = dict(tooluid_value) + temp_tools[tooluid_key] = copy.deepcopy(tooluid_value) self.tools.clear() - self.tools = dict(temp_tools) + self.tools = copy.deepcopy(temp_tools) temp_tools.clear() self.ui_connect() @@ -2936,27 +3055,48 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.toolchangeg_cb.set_value(self.old_toolchangeg_state) self.ui.toolchangeg_cb.setDisabled(False) + if "toolchange_probe" in current_pp.lower(): + self.ui.pdepth_entry.setVisible(True) + self.ui.pdepth_label.show() + + self.ui.feedrate_probe_entry.setVisible(True) + self.ui.feedrate_probe_label.show() + else: + self.ui.pdepth_entry.setVisible(False) + self.ui.pdepth_label.hide() + + self.ui.feedrate_probe_entry.setVisible(False) + self.ui.feedrate_probe_label.hide() + def on_generatecnc_button_click(self, *args): self.app.report_usage("geometry_on_generatecnc_button") self.read_form() - # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): - tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text()) + try: + tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong Tool Dia value format entered, " + "use a number.") + return tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid: self.sel_tools.update({ - tooluid: dict(tooluid_value) + tooluid: copy.deepcopy(tooluid_value) }) self.mtool_gen_cncjob() self.ui.geo_tools_table.clearSelection() else: - self.app.inform.emit("[error_notcl] Failed. No tool selected in the tool table ...") + self.app.inform.emit("[ERROR_NOTCL] Failed. No tool selected in the tool table ...") def mtool_gen_cncjob(self, segx=None, segy=None, use_thread=True): """ @@ -2985,6 +3125,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): segx = segx if segx is not None else float(self.app.defaults['geometry_segx']) segy = segy if segy is not None else float(self.app.defaults['geometry_segy']) + xmin = self.options['xmin'] + ymin = self.options['ymin'] + xmax = self.options['xmax'] + ymax = self.options['ymax'] + # Object initialization function for app.new_object() # RUNNING ON SEPARATE THREAD! def job_init_single_geometry(job_obj, app_obj): @@ -3006,6 +3151,27 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): job_obj.segx = segx job_obj.segy = segy + try: + job_obj.z_pdepth = float(self.options["z_pdepth"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') + + try: + job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + 'or self.options["feedrate_probe"]') + for tooluid_key in self.sel_tools: tool_cnt += 1 app_obj.progress.emit(20) @@ -3077,7 +3243,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): if data_key == "dwelltime": dwelltime = data_value - datadict = dict(diadict_value) + datadict = copy.deepcopy(diadict_value) dia_cnc_dict.update({ diadict_key: datadict }) @@ -3093,12 +3259,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tool_offset = 0.0 else: offset_str = 'custom' - offset_value = self.ui.tool_offset_entry.get_value() + try: + offset_value = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit( - "[warning] Tool Offset is selected in Tool Table but no value is provided.\n" + "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n" "Add a Tool Offset or change the Offset Type." ) return @@ -3114,9 +3290,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia_val + job_obj.options['xmin'] = xmin + job_obj.options['ymin'] = ymin + job_obj.options['xmax'] = xmax + job_obj.options['ymax'] = ymax + app_obj.progress.emit(40) - dia_cnc_dict['gcode'] = job_obj.generate_from_geometry_2( + res = job_obj.generate_from_geometry_2( self, tooldia=tooldia_val, offset=tool_offset, tolerance=0.0005, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, @@ -3127,10 +3308,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) + if res == 'fail': + log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") + return 'fail' + else: + dia_cnc_dict['gcode'] = res + app_obj.progress.emit(50) # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode - job_obj.toolchange_xy = "geometry" + job_obj.toolchange_xy_type = "geometry" dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() @@ -3140,7 +3327,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(80) job_obj.cnc_tools.update({ - tooluid_key: dict(dia_cnc_dict) + tooluid_key: copy.deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() @@ -3162,6 +3349,27 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): job_obj.multigeo = True job_obj.cnc_tools.clear() + try: + job_obj.z_pdepth = float(self.options["z_pdepth"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') + + try: + job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + 'or self.options["feedrate_probe"]') + for tooluid_key in self.sel_tools: tool_cnt += 1 app_obj.progress.emit(20) @@ -3243,7 +3451,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): if data_key == "dwelltime": dwelltime = data_value - datadict = dict(diadict_value) + datadict = copy.deepcopy(diadict_value) dia_cnc_dict.update({ diadict_key: datadict }) @@ -3259,12 +3467,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tool_offset = 0.0 else: offset_str = 'custom' - offset_value = self.ui.tool_offset_entry.get_value() + try: + offset_value = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit( - "[warning] Tool Offset is selected in Tool Table but no value is provided.\n" + "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n" "Add a Tool Offset or change the Offset Type." ) return @@ -3283,7 +3501,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(40) tool_solid_geometry = self.tools[current_uid]['solid_geometry'] - dia_cnc_dict['gcode'] = job_obj.generate_from_multitool_geometry( + res = job_obj.generate_from_multitool_geometry( tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, tolerance=0.0005, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, @@ -3294,6 +3512,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) + if res == 'fail': + log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") + return 'fail' + else: + dia_cnc_dict['gcode'] = res + dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() # TODO this serve for bounding box creation only; should be optimized @@ -3301,12 +3525,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode - job_obj.toolchange_xy = "geometry" + job_obj.toolchange_xy_type = "geometry" app_obj.progress.emit(80) job_obj.cnc_tools.update({ - tooluid_key: dict(dia_cnc_dict) + tooluid_key: copy.deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() @@ -3317,14 +3541,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): def job_thread(app_obj): if self.solid_geometry: with self.app.proc_container.new("Generating CNC Code"): - app_obj.new_object("cncjob", outname, job_init_single_geometry) - app_obj.inform.emit("[success]CNCjob created: %s" % outname) - app_obj.progress.emit(100) + if app_obj.new_object("cncjob", outname, job_init_single_geometry) != 'fail': + app_obj.inform.emit("[success]CNCjob created: %s" % outname) + app_obj.progress.emit(100) else: with self.app.proc_container.new("Generating CNC Code"): - app_obj.new_object("cncjob", outname, job_init_multi_geometry) - app_obj.inform.emit("[success]CNCjob created: %s" % outname) - app_obj.progress.emit(100) + if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail': + app_obj.inform.emit("[success]CNCjob created: %s" % outname) + app_obj.progress.emit(100) # Create a promise with the name self.app.collection.promise(outname) @@ -3366,6 +3590,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): :param ppname_g Name of the postprocessor :return: None """ + tooldia = tooldia if tooldia else self.options["cnctooldia"] outname = outname if outname is not None else self.options["name"] @@ -3419,6 +3644,27 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): job_obj.segx = segx job_obj.segy = segy + try: + job_obj.z_pdepth = float(self.options["z_pdepth"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') + + try: + job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + 'or self.options["feedrate_probe"]') + # TODO: The tolerance should not be hard coded. Just for testing. job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=0.0005, z_cut=z_cut, z_move=z_move, @@ -3433,7 +3679,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(50) # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the # source of gcode - job_obj.toolchange_xy = "geometry" + job_obj.toolchange_xy_type = "geometry" job_obj.gcode_parse() app_obj.progress.emit(80) @@ -3473,7 +3719,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): try: xfactor = float(xfactor) except: - self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.") + self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.") return if yfactor is None: @@ -3482,7 +3728,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): try: yfactor = float(yfactor) except: - self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.") + self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.") return if point is None: @@ -3532,7 +3778,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): try: dx, dy = vect except TypeError: - self.app.inform.emit("[error_notcl]An (x,y) pair of values are needed. " + self.app.inform.emit("[ERROR_NOTCL]An (x,y) pair of values are needed. " "Probable you entered only one value in the Offset field.") return @@ -3565,16 +3811,23 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.options['feedrate_rapid'] *= factor self.options['endz'] *= factor # self.options['cnctooldia'] *= factor - self.options['painttooldia'] *= factor - self.options['paintmargin'] *= factor - self.options['paintoverlap'] *= factor + # self.options['painttooldia'] *= factor + # self.options['paintmargin'] *= factor + # self.options['paintoverlap'] *= factor self.options["toolchangez"] *= factor - coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")] - coords_xy[0] *= factor - coords_xy[1] *= factor - self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) + if self.app.defaults["geometry_toolchangexy"] == '': + self.options['toolchangexy'] = "0.0, 0.0" + else: + coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")] + if len(coords_xy) < 2: + self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be " + "in the format (x, y) \nbut now there is only one value, not two. ") + return 'fail' + coords_xy[0] *= factor + coords_xy[1] *= factor + self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) if self.options['startz'] is not None: self.options['startz'] *= factor @@ -3598,7 +3851,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tool_dia_copy[dia_key] = dia_value # convert the value in the Custom Tool Offset entry in UI - custom_offset = self.ui.tool_offset_entry.get_value() + try: + custom_offset = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + if custom_offset: custom_offset *= factor self.ui.tool_offset_entry.set_value(custom_offset) @@ -3616,17 +3880,17 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # copy the other dict entries that are not convertible if data_key not in param_list: data_copy[data_key] = data_value - tool_dia_copy[dia_key] = dict(data_copy) + tool_dia_copy[dia_key] = copy.deepcopy(data_copy) data_copy.clear() temp_tools_dict.update({ - tooluid_key: dict(tool_dia_copy) + tooluid_key: copy.deepcopy(tool_dia_copy) }) tool_dia_copy.clear() self.tools.clear() - self.tools = dict(temp_tools_dict) + self.tools = copy.deepcopy(temp_tools_dict) # if there is a value in the new tool field then convert that one too tooldia = self.ui.addtool_entry.get_value() @@ -3791,11 +4055,22 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # (like the one in the TCL Command), False self.multitool = False - # used for parsing the GCode lines to adjust the offset when the GCode was offseted - offsetx_re_string = r'(?=.*(X[-\+]?\d*\.\d*))' - self.g_offsetx_re = re.compile(offsetx_re_string) - offsety_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))' - self.g_offsety_re = re.compile(offsety_re_string) + # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled + gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))' + self.g_x_re = re.compile(gcodex_re_string) + gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))' + self.g_y_re = re.compile(gcodey_re_string) + gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))' + self.g_z_re = re.compile(gcodez_re_string) + + gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))' + self.g_f_re = re.compile(gcodef_re_string) + gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))' + self.g_t_re = re.compile(gcodet_re_string) + + gcodenr_re_string = r'([+-]?\d*\.\d+)' + self.g_nr_re = re.compile(gcodenr_re_string) + # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. @@ -3936,10 +4211,15 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # Fill form fields only on object create self.to_form() + # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob + self.ui.cncplot_method_combo.set_value('all') + self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click) self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click) self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click) + self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change) + def ui_connect(self): for row in range(self.ui.cnc_tools_table.rowCount()): self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table) @@ -3962,10 +4242,15 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.read_form() self.plot() + def on_plot_kind_change(self): + kind = self.ui.cncplot_method_combo.get_value() + self.plot(kind=kind) + def on_exportgcode_button_click(self, *args): self.app.report_usage("cncjob_on_exportgcode_button") self.read_form() + name = self.app.collection.get_active().options['name'] if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name: _filter_ = "RML1 Files (*.rol);;" \ @@ -3976,12 +4261,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): else: _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \ "G-Code Files (*.g-code);;All Files (*.*)" + try: filename = str(QtWidgets.QFileDialog.getSaveFileName( - caption="Export Machine Code ...", directory=self.app.get_last_save_folder(), filter=_filter_)[0]) + caption="Export Machine Code ...", + directory=self.app.get_last_save_folder() + '/' + name, + filter=_filter_ + )[0]) except TypeError: filename = str(QtWidgets.QFileDialog.getSaveFileName(caption="Export Machine Code ...", filter=_filter_)[0]) + if filename == '': + self.app.inform.emit("[WARNING_NOTCL]Export Machine Code cancelled ...") + return + preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) @@ -4003,7 +4296,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) - + # print(self.app.gcode_edited) # first clear previous text in text editor (if any) self.app.ui.code_editor.clear() @@ -4022,6 +4315,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) marlin = False hpgl = False + probe_pp = False try: for key in self.cnc_tools: @@ -4031,15 +4325,23 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl': hpgl = True break + if "toolchange_probe" in self.cnc_tools[key]['data']['ppname_g'].lower(): + probe_pp = True + break except Exception as e: log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e)) - try: - for key in self.cnc_tools: - if self.cnc_tools[key]['data']['ppname_e'] == 'marlin': - marlin = True - break - except: - pass + + try: + if self.options['ppname_e'] == 'marlin': + marlin = True + except Exception as e: + log.debug("FlatCAMCNCJob.gcode_header(): --> %s" % str(e)) + + try: + if "toolchange_probe" in self.options['ppname_e'].lower(): + probe_pp = True + except Exception as e: + log.debug("FlatCAMCNCJob.gcode_header(): --> %s" % str(e)) if marlin is True: gcode = ';Marlin G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \ @@ -4065,6 +4367,24 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): gcode += 'CO "Units: ' + self.units.upper() + '";\n' gcode += 'CO "Created on ' + time_str + '";\n' + elif probe_pp is True: + gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ + (str(self.app.version), str(self.app.version_date)) + '\n' + + gcode += '(This GCode tool change is done by using a Probe.)\n' \ + '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \ + '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \ + '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \ + 'Then zero the Z axis.)\n' + '\n' + + gcode += '(Name: ' + str(self.options['name']) + ')\n' + gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n' + + # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' + + gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" + gcode += '(Created on ' + time_str + ')\n' + '\n' else: gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ (str(self.app.version), str(self.app.version_date)) + '\n' @@ -4131,7 +4451,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # if it did not find 'G20' and it did not find 'G21' then there is an error and return if g_idx == -1: - self.app.inform.emit("[error_notcl] G-code does not have a units code: either G20 or G21") + self.app.inform.emit("[ERROR_NOTCL] G-code does not have a units code: either G20 or G21") return g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble @@ -4147,7 +4467,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): f.write(line) except FileNotFoundError: - self.app.inform.emit("[warning_notcl] No such file or directory") + self.app.inform.emit("[WARNING_NOTCL] No such file or directory") return elif to_file is False: # Just for adding it to the recent files list. @@ -4218,7 +4538,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.ui_connect() - def plot(self, visible=None): + def plot(self, visible=None, kind='all'): # Does all the required setup and returns False # if the 'ptint' option is set to False. @@ -4229,13 +4549,13 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): try: if self.multitool is False: # single tool usage - self.plot2(tooldia=self.options["tooldia"], obj=self, visible=visible) + self.plot2(tooldia=self.options["tooldia"], obj=self, visible=visible, kind=kind) else: # multiple tools usage for tooluid_key in self.cnc_tools: tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia'])) gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed'] - self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed) + self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) @@ -4246,4 +4566,63 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()") self.options["tooldia"] *= factor + param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid', + 'endz', 'toolchangez'] + + temp_tools_dict = {} + tool_dia_copy = {} + data_copy = {} + + for tooluid_key, tooluid_value in self.cnc_tools.items(): + for dia_key, dia_value in tooluid_value.items(): + if dia_key == 'tooldia': + dia_value *= factor + dia_value = float('%.4f' % dia_value) + tool_dia_copy[dia_key] = dia_value + if dia_key == 'offset': + tool_dia_copy[dia_key] = dia_value + if dia_key == 'offset_value': + dia_value *= factor + tool_dia_copy[dia_key] = dia_value + + if dia_key == 'type': + tool_dia_copy[dia_key] = dia_value + if dia_key == 'tool_type': + tool_dia_copy[dia_key] = dia_value + if dia_key == 'data': + for data_key, data_value in dia_value.items(): + # convert the form fields that are convertible + for param in param_list: + if data_key == param and data_value is not None: + data_copy[data_key] = data_value * factor + # copy the other dict entries that are not convertible + if data_key not in param_list: + data_copy[data_key] = data_value + tool_dia_copy[dia_key] = copy.deepcopy(data_copy) + data_copy.clear() + + if dia_key == 'gcode': + tool_dia_copy[dia_key] = dia_value + if dia_key == 'gcode_parsed': + tool_dia_copy[dia_key] = dia_value + if dia_key == 'solid_geometry': + tool_dia_copy[dia_key] = dia_value + + # if dia_key == 'solid_geometry': + # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0)) + # if dia_key == 'gcode_parsed': + # for g in dia_value: + # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0)) + # + # tool_dia_copy['gcode_parsed'] = copy.deepcopy(dia_value) + # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value]) + + temp_tools_dict.update({ + tooluid_key: copy.deepcopy(tool_dia_copy) + }) + tool_dia_copy.clear() + + self.cnc_tools.clear() + self.cnc_tools = copy.deepcopy(temp_tools_dict) + # end of file diff --git a/GUIElements.py b/GUIElements.py index 30a1e856..d2242559 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -1,4 +1,6 @@ -from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets +from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtCore import pyqtSignal, pyqtSlot + from copy import copy import re import logging @@ -550,6 +552,478 @@ class FCTab(QtWidgets.QTabWidget): self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None) +class FCDetachableTab(QtWidgets.QTabWidget): + # From here: https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget + def __init__(self, protect=None, protect_by_name=None, parent=None): + + super().__init__() + + self.tabBar = self.FCTabBar(self) + self.tabBar.onDetachTabSignal.connect(self.detachTab) + self.tabBar.onMoveTabSignal.connect(self.moveTab) + self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop) + + self.setTabBar(self.tabBar) + + # Used to keep a reference to detached tabs since their QMainWindow + # does not have a parent + self.detachedTabs = {} + + # a way to make sure that tabs can't be closed after they attach to the parent tab + self.protect_tab = True if protect is not None and protect is True else False + + self.protect_by_name = protect_by_name if isinstance(protect_by_name, list) else None + + # Close all detached tabs if the application is closed explicitly + QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable + + # used by the property self.useOldIndex(param) + self.use_old_index = None + self.old_index = None + + self.setTabsClosable(True) + self.tabCloseRequested.connect(self.closeTab) + + def useOldIndex(self, param): + if param: + self.use_old_index = True + else: + self.use_old_index = False + + def deleteTab(self, currentIndex): + widget = self.widget(currentIndex) + if widget is not None: + widget.deleteLater() + self.removeTab(currentIndex) + + def closeTab(self, currentIndex): + self.removeTab(currentIndex) + + def protectTab(self, currentIndex): + # self.FCTabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None) + self.tabBar.setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None) + + ## + # The default movable functionality of QTabWidget must remain disabled + # so as not to conflict with the added features + def setMovable(self, movable): + pass + + ## + # Move a tab from one position (index) to another + # + # @param fromIndex the original index location of the tab + # @param toIndex the new index location of the tab + @pyqtSlot(int, int) + def moveTab(self, fromIndex, toIndex): + widget = self.widget(fromIndex) + icon = self.tabIcon(fromIndex) + text = self.tabText(fromIndex) + + self.removeTab(fromIndex) + self.insertTab(toIndex, widget, icon, text) + self.setCurrentIndex(toIndex) + + ## + # Detach the tab by removing it's contents and placing them in + # a DetachedTab window + # + # @param index the index location of the tab to be detached + # @param point the screen position for creating the new DetachedTab window + @pyqtSlot(int, QtCore.QPoint) + def detachTab(self, index, point): + + self.old_index = index + + # Get the tab content + name = self.tabText(index) + icon = self.tabIcon(index) + if icon.isNull(): + icon = self.window().windowIcon() + contentWidget = self.widget(index) + + try: + contentWidgetRect = contentWidget.frameGeometry() + except AttributeError: + return + + # Create a new detached tab window + detachedTab = self.FCDetachedTab(name, contentWidget) + detachedTab.setWindowModality(QtCore.Qt.NonModal) + detachedTab.setWindowIcon(icon) + detachedTab.setGeometry(contentWidgetRect) + detachedTab.onCloseSignal.connect(self.attachTab) + detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop) + detachedTab.move(point) + detachedTab.show() + + + # Create a reference to maintain access to the detached tab + self.detachedTabs[name] = detachedTab + + + ## + # Re-attach the tab by removing the content from the DetachedTab window, + # closing it, and placing the content back into the DetachableTabWidget + # + # @param contentWidget the content widget from the DetachedTab window + # @param name the name of the detached tab + # @param icon the window icon for the detached tab + # @param insertAt insert the re-attached tab at the given index + def attachTab(self, contentWidget, name, icon, insertAt=None): + + # Make the content widget a child of this widget + contentWidget.setParent(self) + + # Remove the reference + del self.detachedTabs[name] + + # helps in restoring the tab to the same index that it was before was detached + insert_index = self.old_index if self.use_old_index is True else insertAt + + # Create an image from the given icon (for comparison) + if not icon.isNull(): + try: + tabIconPixmap = icon.pixmap(icon.availableSizes()[0]) + tabIconImage = tabIconPixmap.toImage() + except IndexError: + tabIconImage = None + else: + tabIconImage = None + + # Create an image of the main window icon (for comparison) + if not icon.isNull(): + try: + windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0]) + windowIconImage = windowIconPixmap.toImage() + except IndexError: + windowIconImage = None + else: + windowIconImage = None + + # Determine if the given image and the main window icon are the same. + # If they are, then do not add the icon to the tab + if tabIconImage == windowIconImage: + if insert_index is None: + index = self.addTab(contentWidget, name) + else: + index = self.insertTab(insert_index, contentWidget, name) + else: + if insert_index is None: + index = self.addTab(contentWidget, icon, name) + else: + index = self.insertTab(insert_index, contentWidget, icon, name) + + # on reattaching the tab if protect is true then the closure button is not added + if self.protect_tab is True: + self.protectTab(index) + + # on reattaching the tab disable the closure button for the tabs with the name in the self.protect_by_name list + if self.protect_by_name is not None: + for tab_name in self.protect_by_name: + for index in range(self.count()): + if str(tab_name) == str(self.tabText(index)): + self.protectTab(index) + + # Make this tab the current tab + if index > -1: + self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index) + + ## + # Remove the tab with the given name, even if it is detached + # + # @param name the name of the tab to be removed + def removeTabByName(self, name): + + # Remove the tab if it is attached + attached = False + for index in range(self.count()): + if str(name) == str(self.tabText(index)): + self.removeTab(index) + attached = True + break + + + # If the tab is not attached, close it's window and + # remove the reference to it + if not attached: + for key in self.detachedTabs: + if str(name) == str(key): + self.detachedTabs[key].onCloseSignal.disconnect() + self.detachedTabs[key].close() + del self.detachedTabs[key] + break + + + ## + # Handle dropping of a detached tab inside the DetachableTabWidget + # + # @param name the name of the detached tab + # @param index the index of an existing tab (if the tab bar + # determined that the drop occurred on an + # existing tab) + # @param dropPos the mouse cursor position when the drop occurred + @QtCore.pyqtSlot(str, int, QtCore.QPoint) + def detachedTabDrop(self, name, index, dropPos): + + # If the drop occurred on an existing tab, insert the detached + # tab at the existing tab's location + if index > -1: + + # Create references to the detached tab's content and icon + contentWidget = self.detachedTabs[name].contentWidget + icon = self.detachedTabs[name].windowIcon() + + # Disconnect the detached tab's onCloseSignal so that it + # does not try to re-attach automatically + self.detachedTabs[name].onCloseSignal.disconnect() + + # Close the detached + self.detachedTabs[name].close() + + # Re-attach the tab at the given index + self.attachTab(contentWidget, name, icon, index) + + + # If the drop did not occur on an existing tab, determine if the drop + # occurred in the tab bar area (the area to the side of the QTabBar) + else: + + # Find the drop position relative to the DetachableTabWidget + tabDropPos = self.mapFromGlobal(dropPos) + + # If the drop position is inside the DetachableTabWidget... + if self.rect().contains(tabDropPos): + + # If the drop position is inside the tab bar area (the + # area to the side of the QTabBar) or there are not tabs + # currently attached... + if tabDropPos.y() < self.tabBar.height() or self.count() == 0: + + # Close the detached tab and allow it to re-attach + # automatically + self.detachedTabs[name].close() + + + ## + # Close all tabs that are currently detached. + def closeDetachedTabs(self): + listOfDetachedTabs = [] + + for key in self.detachedTabs: + listOfDetachedTabs.append(self.detachedTabs[key]) + + for detachedTab in listOfDetachedTabs: + detachedTab.close() + + + ## + # When a tab is detached, the contents are placed into this QMainWindow. The tab + # can be re-attached by closing the dialog or by dragging the window into the tab bar + class FCDetachedTab(QtWidgets.QMainWindow): + onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon) + onDropSignal = pyqtSignal(str, QtCore.QPoint) + + def __init__(self, name, contentWidget): + QtWidgets.QMainWindow.__init__(self, None) + + self.setObjectName(name) + self.setWindowTitle(name) + + self.contentWidget = contentWidget + self.setCentralWidget(self.contentWidget) + self.contentWidget.show() + + self.windowDropFilter = self.WindowDropFilter() + self.installEventFilter(self.windowDropFilter) + self.windowDropFilter.onDropSignal.connect(self.windowDropSlot) + + + ## + # Handle a window drop event + # + # @param dropPos the mouse cursor position of the drop + @QtCore.pyqtSlot(QtCore.QPoint) + def windowDropSlot(self, dropPos): + self.onDropSignal.emit(self.objectName(), dropPos) + + + ## + # If the window is closed, emit the onCloseSignal and give the + # content widget back to the DetachableTabWidget + # + # @param event a close event + def closeEvent(self, event): + self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon()) + + + ## + # An event filter class to detect a QMainWindow drop event + class WindowDropFilter(QtCore.QObject): + onDropSignal = pyqtSignal(QtCore.QPoint) + + def __init__(self): + QtCore.QObject.__init__(self) + self.lastEvent = None + + + ## + # Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173) + # event that immediately follows a Move event + # + # @param obj the object that generated the event + # @param event the current event + def eventFilter(self, obj, event): + + # If a NonClientAreaMouseMove (173) event immediately follows a Move event... + if self.lastEvent == QtCore.QEvent.Move and event.type() == 173: + + # Determine the position of the mouse cursor and emit it with the + # onDropSignal + mouseCursor = QtGui.QCursor() + dropPos = mouseCursor.pos() + self.onDropSignal.emit(dropPos) + self.lastEvent = event.type() + return True + + else: + self.lastEvent = event.type() + return False + + class FCTabBar(QtWidgets.QTabBar): + onDetachTabSignal = pyqtSignal(int, QtCore.QPoint) + onMoveTabSignal = pyqtSignal(int, int) + detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint) + + def __init__(self, parent=None): + QtWidgets.QTabBar.__init__(self, parent) + + self.setAcceptDrops(True) + self.setElideMode(QtCore.Qt.ElideRight) + self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab) + + self.dragStartPos = QtCore.QPoint() + self.dragDropedPos = QtCore.QPoint() + self.mouseCursor = QtGui.QCursor() + self.dragInitiated = False + + + # Send the onDetachTabSignal when a tab is double clicked + # + # @param event a mouse double click event + def mouseDoubleClickEvent(self, event): + event.accept() + self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos()) + + + # Set the starting position for a drag event when the mouse button is pressed + # + # @param event a mouse press event + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.dragStartPos = event.pos() + + self.dragDropedPos.setX(0) + self.dragDropedPos.setY(0) + + self.dragInitiated = False + + QtWidgets.QTabBar.mousePressEvent(self, event) + + + # Determine if the current movement is a drag. If it is, convert it into a QDrag. If the + # drag ends inside the tab bar, emit an onMoveTabSignal. If the drag ends outside the tab + # bar, emit an onDetachTabSignal. + # + # @param event a mouse move event + def mouseMoveEvent(self, event): + + # Determine if the current movement is detected as a drag + if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()): + self.dragInitiated = True + + # If the current movement is a drag initiated by the left button + if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated): + + # Stop the move event + finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier) + QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent) + + # Convert the move event into a drag + drag = QtGui.QDrag(self) + mimeData = QtCore.QMimeData() + # mimeData.setData('action', 'application/tab-detach') + drag.setMimeData(mimeData) + # screen = QScreen(self.parentWidget().currentWidget().winId()) + # Create the appearance of dragging the tab content + try: + pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab() + except Exception as e: + log.debug("GUIElements.FCDetachable. FCTabBar.mouseMoveEvent() --> %s" % str(e)) + return + + targetPixmap = QtGui.QPixmap(pixmap.size()) + targetPixmap.fill(QtCore.Qt.transparent) + painter = QtGui.QPainter(targetPixmap) + painter.setOpacity(0.85) + painter.drawPixmap(0, 0, pixmap) + painter.end() + drag.setPixmap(targetPixmap) + + # Initiate the drag + dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction) + + + # For Linux: Here, drag.exec_() will not return MoveAction on Linux. So it + # must be set manually + if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0: + dropAction = QtCore.Qt.MoveAction + + + # If the drag completed outside of the tab bar, detach the tab and move + # the content to the current cursor position + if dropAction == QtCore.Qt.IgnoreAction: + event.accept() + self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos()) + + # Else if the drag completed inside the tab bar, move the selected tab to the new position + elif dropAction == QtCore.Qt.MoveAction: + if not self.dragDropedPos.isNull(): + event.accept() + self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos)) + else: + QtWidgets.QTabBar.mouseMoveEvent(self, event) + + # Determine if the drag has entered a tab position from another tab position + # + # @param event a drag enter event + def dragEnterEvent(self, event): + mimeData = event.mimeData() + # formats = mcd imeData.formats() + + # if formats.contains('action') and mimeData.data('action') == 'application/tab-detach': + # event.acceptProposedAction() + + QtWidgets.QTabBar.dragMoveEvent(self, event) + + # Get the position of the end of the drag + # + # @param event a drop event + def dropEvent(self, event): + self.dragDropedPos = event.pos() + QtWidgets.QTabBar.dropEvent(self, event) + + # Determine if the detached tab drop event occurred on an existing tab, + # then send the event to the DetachableTabWidget + def detachedTabDrop(self, name, dropPos): + + tabDropPos = self.mapFromGlobal(dropPos) + + index = self.tabAt(tabDropPos) + + self.detachedTabDropSignal.emit(name, index, dropPos) + + class VerticalScrollArea(QtWidgets.QScrollArea): """ This widget extends QtGui.QScrollArea to make a vertical-only diff --git a/ObjectCollection.py b/ObjectCollection.py index 9c955b5a..ff967a74 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -46,57 +46,66 @@ class KeySensitiveListView(QtWidgets.QTreeView): event.ignore() def dragMoveEvent(self, event): + self.setDropIndicatorShown(True) if event.mimeData().hasUrls: event.accept() else: event.ignore() def dropEvent(self, event): - if event.mimeData().hasUrls: - event.setDropAction(QtCore.Qt.CopyAction) + drop_indicator = self.dropIndicatorPosition() + + m = event.mimeData() + if m.hasUrls: event.accept() - for url in event.mimeData().urls(): + + for url in m.urls(): self.filename = str(url.toLocalFile()) - if self.filename == "": - self.app.inform.emit("Open cancelled.") + # file drop from outside application + if drop_indicator == QtWidgets.QAbstractItemView.OnItem: + if self.filename == "": + self.app.inform.emit("Open cancelled.") + else: + if self.filename.lower().rpartition('.')[-1] in self.app.grb_list: + self.app.worker_task.emit({'fcn': self.app.open_gerber, + 'params': [self.filename]}) + else: + event.ignore() + + if self.filename.lower().rpartition('.')[-1] in self.app.exc_list: + self.app.worker_task.emit({'fcn': self.app.open_excellon, + 'params': [self.filename]}) + else: + event.ignore() + + if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list: + self.app.worker_task.emit({'fcn': self.app.open_gcode, + 'params': [self.filename]}) + else: + event.ignore() + + if self.filename.lower().rpartition('.')[-1] in self.app.svg_list: + object_type = 'geometry' + self.app.worker_task.emit({'fcn': self.app.import_svg, + 'params': [self.filename, object_type, None]}) + + if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list: + object_type = 'geometry' + self.app.worker_task.emit({'fcn': self.app.import_dxf, + 'params': [self.filename, object_type, None]}) + + if self.filename.lower().rpartition('.')[-1] in self.app.prj_list: + # self.app.open_project() is not Thread Safe + self.app.open_project(self.filename) + else: + event.ignore() else: - if self.filename.lower().rpartition('.')[-1] in self.app.grb_list: - self.app.worker_task.emit({'fcn': self.app.open_gerber, - 'params': [self.filename]}) - else: - event.ignore() - - if self.filename.lower().rpartition('.')[-1] in self.app.exc_list: - self.app.worker_task.emit({'fcn': self.app.open_excellon, - 'params': [self.filename]}) - else: - event.ignore() - - if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list: - self.app.worker_task.emit({'fcn': self.app.open_gcode, - 'params': [self.filename]}) - else: - event.ignore() - - if self.filename.lower().rpartition('.')[-1] in self.app.svg_list: - object_type = 'geometry' - self.app.worker_task.emit({'fcn': self.app.import_svg, - 'params': [self.filename, object_type, None]}) - - if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list: - object_type = 'geometry' - self.app.worker_task.emit({'fcn': self.app.import_dxf, - 'params': [self.filename, object_type, None]}) - - if self.filename.lower().rpartition('.')[-1] in self.app.prj_list: - # self.app.open_project() is not Thread Safe - self.app.open_project(self.filename) - else: - event.ignore() + pass else: event.ignore() + class TreeItem: """ Item of a tree model @@ -221,9 +230,15 @@ class ObjectCollection(QtCore.QAbstractItemModel): ### View self.view = KeySensitiveListView(app) - self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.view.setModel(self) + self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + # self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + # self.view.setDragEnabled(True) + # self.view.setAcceptDrops(True) + # self.view.setDropIndicatorShown(True) + font = QtGui.QFont() font.setPixelSize(12) font.setFamily("Seagoe UI") @@ -273,6 +288,11 @@ class ObjectCollection(QtCore.QAbstractItemModel): if key == QtCore.Qt.Key_S: self.app.on_file_saveproject() + + # Toggle Plot Area + if key == QtCore.Qt.Key_F10: + self.app.on_toggle_plotarea() + return elif modifiers == QtCore.Qt.ShiftModifier: @@ -324,7 +344,20 @@ class ObjectCollection(QtCore.QAbstractItemModel): if key == QtCore.Qt.Key_Y: self.app.on_skewy() return + elif modifiers == QtCore.Qt.AltModifier: + # Eanble all plots + if key == Qt.Key_1: + self.app.enable_all_plots() + + # Disable all plots + if key == Qt.Key_2: + self.app.disable_all_plots() + + # Disable all other plots + if key == Qt.Key_3: + self.app.disable_other_plots() + # 2-Sided PCB Tool if key == QtCore.Qt.Key_D: self.app.dblsidedtool.run() @@ -354,17 +387,17 @@ class ObjectCollection(QtCore.QAbstractItemModel): if key == QtCore.Qt.Key_F2: webbrowser.open(self.app.video_url) - # Zoom Fit + # Switch to Project Tab if key == QtCore.Qt.Key_1: - self.app.on_zoom_fit(None) + self.app.on_select_tab('project') - # Zoom In + # Switch to Selected Tab if key == QtCore.Qt.Key_2: - self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse) + self.app.on_select_tab('selected') - # Zoom Out + # Switch to Tool Tab if key == QtCore.Qt.Key_3: - self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse) + self.app.on_select_tab('tool') # Delete if key == QtCore.Qt.Key_Delete and active: @@ -444,6 +477,14 @@ class ObjectCollection(QtCore.QAbstractItemModel): if key == QtCore.Qt.Key_Y: self.app.on_flipy() + # Zoom In + if key == QtCore.Qt.Key_Equal: + self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse) + + # Zoom Out + if key == QtCore.Qt.Key_Minus: + self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse) + # Show shortcut list if key == QtCore.Qt.Key_Ampersand: self.app.on_shortcut_list() @@ -483,13 +524,13 @@ class ObjectCollection(QtCore.QAbstractItemModel): if not self.hasIndex(row, column, parent): return QtCore.QModelIndex() - if not parent.isValid(): - parent_item = self.root_item - else: - parent_item = parent.internalPointer() + # if not parent.isValid(): + # parent_item = self.root_item + # else: + # parent_item = parent.internalPointer() + parent_item = parent.internalPointer() if parent.isValid() else self.root_item child_item = parent_item.child(row) - if child_item: return self.createIndex(row, column, child_item) else: @@ -569,39 +610,27 @@ class ObjectCollection(QtCore.QAbstractItemModel): "setData() --> Could not remove the old object name from auto-completer model list") obj.build_ui() - self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name)) + self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name)) return True + def supportedDropActions(self): + return Qt.MoveAction + def flags(self, index): + default_flags = QtCore.QAbstractItemModel.flags(self, index) + if not index.isValid(): - return 0 + return Qt.ItemIsEnabled | default_flags # Prevent groups from selection if not index.internalPointer().obj: return Qt.ItemIsEnabled else: - return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable + return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable | \ + Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled - return QtWidgets.QAbstractItemModel.flags(self, index) - - # def data(self, index, role=Qt.Qt.DisplayRole): - # if not index.isValid() or not 0 <= index.row() < self.rowCount(): - # return QtCore.QVariant() - # row = index.row() - # if role == Qt.Qt.DisplayRole: - # return self.object_list[row].options["name"] - # if role == Qt.Qt.DecorationRole: - # return self.icons[self.object_list[row].kind] - # # if role == Qt.Qt.CheckStateRole: - # # if row in self.checked_indexes: - # # return Qt.Qt.Checked - # # else: - # # return Qt.Qt.Unchecked - - def print_list(self): - for obj in self.get_list(): - print(obj) + # return QtWidgets.QAbstractItemModel.flags(self, index) def append(self, obj, active=False): FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()") @@ -611,8 +640,8 @@ class ObjectCollection(QtCore.QAbstractItemModel): # 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)) + # 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 diff --git a/ObjectUI.py b/ObjectUI.py index d16bf157..fc8a8287 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -556,15 +556,38 @@ class ExcellonObjectUI(ObjectUI): self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry]) # postprocessor selection - pp_excellon_label = QtWidgets.QLabel("Postprocessor") + pp_excellon_label = QtWidgets.QLabel("Postprocessor:") pp_excellon_label.setToolTip( "The json file that dictates\n" "gcode output." ) - self.tools_box.addWidget(pp_excellon_label) self.pp_excellon_name_cb = FCComboBox() self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus) - self.tools_box.addWidget(self.pp_excellon_name_cb) + grid1.addWidget(pp_excellon_label, 10, 0) + grid1.addWidget(self.pp_excellon_name_cb, 10, 1) + + # Probe depth + self.pdepth_label = QtWidgets.QLabel("Probe Z depth:") + self.pdepth_label.setToolTip( + "The maximum depth that the probe is allowed\n" + "to probe. Negative value, in current units." + ) + grid1.addWidget(self.pdepth_label, 11, 0) + self.pdepth_entry = FCEntry() + grid1.addWidget(self.pdepth_entry, 11, 1) + self.pdepth_label.hide() + self.pdepth_entry.setVisible(False) + + # Probe feedrate + self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:") + self.feedrate_probe_label.setToolTip( + "The feedrate used while the probe is probing." + ) + grid1.addWidget(self.feedrate_probe_label, 12, 0) + self.feedrate_probe_entry = FCEntry() + grid1.addWidget(self.feedrate_probe_entry, 12, 1) + self.feedrate_probe_label.hide() + self.feedrate_probe_entry.setVisible(False) choose_tools_label = QtWidgets.QLabel( "Select from the Tools Table above\n" @@ -708,6 +731,8 @@ class GeometryObjectUI(ObjectUI): self.geo_tools_table.setColumnWidth(0, 20) self.geo_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P']) self.geo_tools_table.setColumnHidden(5, True) + # stylesheet = "::section{Background-color:rgb(239,239,245)}" + # self.geo_tools_table.horizontalHeader().setStyleSheet(stylesheet) self.geo_tools_table.horizontalHeaderItem(0).setToolTip( "This is the Tool Number.\n" @@ -758,7 +783,7 @@ class GeometryObjectUI(ObjectUI): "cut and negative for 'inside' cut." ) self.grid1.addWidget(self.tool_offset_lbl, 0, 0) - self.tool_offset_entry = FloatEntry() + self.tool_offset_entry = FCEntry() spacer_lbl = QtWidgets.QLabel(" ") spacer_lbl.setFixedWidth(80) @@ -777,7 +802,7 @@ class GeometryObjectUI(ObjectUI): self.addtool_entry_lbl.setToolTip( "Diameter for the new tool" ) - self.addtool_entry = FloatEntry() + self.addtool_entry = FCEntry() # hlay.addWidget(self.addtool_label) # hlay.addStretch() @@ -1004,11 +1029,34 @@ class GeometryObjectUI(ObjectUI): self.pp_geometry_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus) self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1) + # Probe depth + self.pdepth_label = QtWidgets.QLabel("Probe Z depth:") + self.pdepth_label.setToolTip( + "The maximum depth that the probe is allowed\n" + "to probe. Negative value, in current units." + ) + self.grid3.addWidget(self.pdepth_label, 17, 0) + self.pdepth_entry = FCEntry() + self.grid3.addWidget(self.pdepth_entry, 17, 1) + self.pdepth_label.hide() + self.pdepth_entry.setVisible(False) + + # Probe feedrate + self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:") + self.feedrate_probe_label.setToolTip( + "The feedrate used while the probe is probing." + ) + self.grid3.addWidget(self.feedrate_probe_label, 18, 0) + self.feedrate_probe_entry = FCEntry() + self.grid3.addWidget(self.feedrate_probe_entry, 18, 1) + self.feedrate_probe_label.hide() + self.feedrate_probe_entry.setVisible(False) + warning_lbl = QtWidgets.QLabel( "Add at least one tool in the tool-table.\n" "Click the header to select all, or Ctrl + LMB\n" "for custom selection of tools.") - self.grid3.addWidget(warning_lbl, 17, 0, 1, 2) + self.grid3.addWidget(warning_lbl, 19, 0, 1, 2) # Button self.generate_cnc_button = QtWidgets.QPushButton('Generate') @@ -1067,15 +1115,26 @@ class CNCObjectUI(ObjectUI): self.plot_options_label = QtWidgets.QLabel("Plot Options:") self.custom_box.addWidget(self.plot_options_label) - # # Tool dia for plot - # tdlabel = QtWidgets.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) + self.cncplot_method_label = QtWidgets.QLabel("Plot kind:") + self.cncplot_method_label.setToolTip( + "This selects the kind of geometries on the canvas to plot.\n" + "Those can be either of type 'Travel' which means the moves\n" + "above the work piece or it can be of type 'Cut',\n" + "which means the moves that cut into the material." + ) + + self.cncplot_method_combo = RadioSet([ + {"label": "All", "value": "all"}, + {"label": "Travel", "value": "travel"}, + {"label": "Cut", "value": "cut"} + ], stretch=False) + + f_lay = QtWidgets.QFormLayout() + self.custom_box.addLayout(f_lay) + f_lay.addRow(self.cncplot_method_label, self.cncplot_method_combo) + + e1_lbl = QtWidgets.QLabel('') + self.custom_box.addWidget(e1_lbl) hlay = QtWidgets.QHBoxLayout() self.custom_box.addLayout(hlay) @@ -1115,6 +1174,8 @@ class CNCObjectUI(ObjectUI): self.cnc_tools_table.setColumnWidth(0, 20) self.cnc_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P']) self.cnc_tools_table.setColumnHidden(5, True) + # stylesheet = "::section{Background-color:rgb(239,239,245)}" + # self.cnc_tools_table.horizontalHeader().setStyleSheet(stylesheet) # Update plot button self.updateplot_button = QtWidgets.QPushButton('Update Plot') diff --git a/ParseFont.py b/ParseFont.py index 5ecc85c4..554ca03b 100644 --- a/ParseFont.py +++ b/ParseFont.py @@ -287,8 +287,8 @@ class ParseFont(): elif font_type == 'regular': path_filename = regular_dict[font_name] except Exception as e: - self.app.inform.emit("[error_notcl] Font not supported, try another one.") - log.debug("[error_notcl] Font Loading: %s" % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Font not supported, try another one.") + log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e)) return "flatcam font parse failed" face = ft.Face(path_filename) diff --git a/ParseSVG.py b/ParseSVG.py index 38bc51c5..c2ad5010 100644 --- a/ParseSVG.py +++ b/ParseSVG.py @@ -121,7 +121,7 @@ def path2shapely(path, object_type, res=1.0): # geo_element = Polygon(points) geo_element = LineString(points) else: - log.error("[error]: Not a valid target object.") + log.error("[ERROR]: Not a valid target object.") if not points: continue else: @@ -639,7 +639,7 @@ def parse_svg_transform(trstr): continue # raise Exception("Don't know how to parse: %s" % trstr) - log.error("[error] Don't know how to parse: %s" % trstr) + log.error("[ERROR] Don't know how to parse: %s" % trstr) return trlist diff --git a/README.md b/README.md index df86cb27..7b09ea0a 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,93 @@ CAD program, and create G-Code for Isolation routing. ================================================= +6.02.2019 + +- fixed the units calculators crash FlatCAM when using comma as decimal separator + +5.02.3019 + +- added a text in the Selected Tab which is showed whenever the Selected Tab is selected but without having an object selected to display it's properties +- added an initial text in the Tools tab +- added possibility to use the shortcut key for shortcut list in the Notebook tabs +- added a way to set the Probe depth if Toolchange_Probe postprocessors are selected +- finished the postprocessor file for MACH3 tool probing on toolchange event +- added a new parameter to set the feedrate of the probing in case the used postprocessor does probing (has toolchange_probe in it's name) +- fixed bug in Marlin postprocessor for the Excellon files; the header and toolchange event always used the parenthesis witch is not compatible with GCode for Marlin +- fixed a issue with a move to Z_move before any toolchange + +4.02.2019 + +- modified the Toolchange_Probe_general postprocessor file to remove any Z moves before the actual toolchange event +- created a prototype postprocessor file for usage with tool probing in MACH3 +- added the default values for Tool Film and Tool Panelize to the Edit -> Preferences +- added a new parameter in the Tool Film which control the thickness of the stroke width in the resulting SVG. It's a scale parameter. +- whatever was the visibility of the corresponding toolbar when we enter in the Editor, it will be set after exit from the Editor (either Geometry Editor or Excellon Editor). +- added ability to be detached for the tabs in the Notebook section (Project, Selected and Tool) +- added ability for all detachable tabs to be restored to the same position from where they were detached. +- changed the shortcut keys for Zoom In, Zoom Out and Zoom Fit from 1, 2, 3 to '-', '=' respectively 'V'. Added new shortcut keys '1', '2', '3' for Select Project Tab, Select Selected Tab and Select Tool Tab. +- formatted the Shortcut List Tab into a HTML table + +3.3.2019 + +- updated the new shortcut list with the shortcuts added lately +- now the special messages in the Shell are color coded according to the level. Before they all were RED. Now the WARNINGS are yellow, ERRORS are red and SUCCESS is a dark green. Also the level is in CAPS LOCK to make them more obvious +- some more changes to GUI interface (solved issues) +- added some status bar messages in the Geometry Editor to guide the user when using the Geometry Tools +- now the '`' shortcut key that shows the 'shortcut key list' in Editors points to the same window which is created in a tab no longer as a pop-up window. This tab can be detached if needed. +- added a remove_tools() function before install_tools() in the init_tools() that is called when creating a new project. Should solve the issue with having double menu entry's in the TOOLS menu +- fixed remove_tools() so the Tcl Shell action is readded to the Tools menu and reconnected to it's slot function +- added an automatic name on each save operation based on the object name and/or the current date +- added more information's for the statistics + +2.2.2019 + +- code cleanup in Tools +- some GUI structure optimization's +- added protection against entering float numbers with comma separator instead of decimal dot separator in key points of FlatCAM (not everywhere) +- added a choice of plotting the kind of geometry for the CNC plot (all, travel and cut kind of geometries) in CNCJob Selected Tab +- added a new postprocessor file named: 'probe_from_zmove' which allow probing to be done from z_move position on toolchange event +- fixed the snap magnet button in Geometry Editor, restored the checkable property to True +- some more changes in the Editors GUI in deactivate() function +- a fix for saving as empty an edited new and empty Excellon Object + +1.02.2019 + +- fixed postprocessor files so now the bounds values are right aligned (assuming max string length of 9 chars which means 4 digits and 4 decimals) +- corrected small type in list_sys Tcl command; added a protection of the Plot Area Tab after a successful edit. +- remade the way FlatCAM saves the GUI position data from a file (previously) to use PyQt QSettings +- added a 'theme' combo selection in Edit -> Preferences. Two themes are available: standard and compact. +- some code cleanup +- fixed a source of possible errors in DetachableTab Widget. +- fixed gcode conversion/scale (on units change) when multiple values are found on each line +- replaced the pop-up window for the shortcut list with a new detachable tab +- removed the pop-up messages from the rotate, skew, flip commands + +31.01.2019 + +- added a parameter ('Fast plunge' in Edit -> Preferences -> Geometry Options and Excellon Options) to control if the fast move to Z_move is done or not +- added new function to toggle fullscreen status in Menu -> View -> Toggle Full Screen. Shortcut key: Alt+F10 +- added key shortcuts for Enable Plots, Disable Plots and Disable other plots functions (Alt+1, Alt+2, Alt+3) +- hidden the snap magnet entry and snap magnet toggle from the main view; they are now active only in Editor Mode +- updated the camlib.CNCJob.scale() function so now the GCode is scaled also (quite a HACK :( it will need to be replaced at some point)). Units change work now on the GCODE also. +- added the bounds coordinates to the GCODE header +- FlatCAM saves now to a file in self.data_path the toolbar positions and the position of TCL Shell +- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key CTRL+F10 +- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop. + 30.01.2019 - added a space before Y coordinate in end_code() function in some of the postprocessor files +- added in Calculators Tool an Electroplating Calculator. +- remade the App Menu for Editors: now they will be showed only when the respective Editor is active and hidden when the Editor is closed. +- added a traceback report in the TCL Shell for the errors that don't allow creation of an object; useful to trace exceptions/errors +- in case that the Toolchange X,Y parameter in Selected (or in Preferences) are deleted then the app will still do the job using the current coordinates for toolchange +- fixed an issue in camlib.CNCJob where tha variable self.toolchange_xy was used for 2 different purposes which created loss of information. +- fixed unit conversion functions in case the toolchange_xy parameter is None +- more fixes in camlib.CNCJob regarding usage of toolchange (in case it is None) +- fixed postprocessor files to work with toolchange_xy parameter value = None (no values in Edit - Preferences fields) +- fixed Tcl commands CncJob and DrillCncJob to work with toolchange +- added to the postprocessor files the command after toolchange to go with G00 (fastest) to "Z Move" value of Z pozition. 29.01.2019 @@ -243,7 +327,7 @@ CAD program, and create G-Code for Isolation routing. - solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable - added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool - Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool -- added a new type of warning, [warning_notcl] +- added a new type of warning, [WARNING_NOTCL] - fixed conflict with "space" keyboard shortcut for CNC job 16.12.2018 diff --git a/camlib.py b/camlib.py index f544e487..1178b716 100644 --- a/camlib.py +++ b/camlib.py @@ -186,7 +186,7 @@ class Geometry(object): if isinstance(self.solid_geometry, list): return len(self.solid_geometry) == 0 - self.app.inform.emit("[error_notcl] self.solid_geometry is neither BaseGeometry or list.") + self.app.inform.emit("[ERROR_NOTCL] self.solid_geometry is neither BaseGeometry or list.") return def subtract_polygon(self, points): @@ -300,7 +300,7 @@ class Geometry(object): # else: # return self.solid_geometry.bounds # except Exception as e: - # self.app.inform.emit("[error_notcl] Error cause: %s" % str(e)) + # self.app.inform.emit("[ERROR_NOTCL] Error cause: %s" % str(e)) # log.debug("Geometry->bounds()") # if self.solid_geometry is None: @@ -1361,7 +1361,7 @@ class Geometry(object): self.solid_geometry = mirror_geom(self.solid_geometry) self.app.inform.emit('[success]Object was mirrored ...') except AttributeError: - self.app.inform.emit("[error_notcl] Failed to mirror. No object selected") + self.app.inform.emit("[ERROR_NOTCL] Failed to mirror. No object selected") @@ -1401,7 +1401,7 @@ class Geometry(object): self.solid_geometry = rotate_geom(self.solid_geometry) self.app.inform.emit('[success]Object was rotated ...') except AttributeError: - self.app.inform.emit("[error_notcl] Failed to rotate. No object selected") + self.app.inform.emit("[ERROR_NOTCL] Failed to rotate. No object selected") def skew(self, angle_x, angle_y, point): """ @@ -1437,7 +1437,7 @@ class Geometry(object): self.solid_geometry = skew_geom(self.solid_geometry) self.app.inform.emit('[success]Object was skewed ...') except AttributeError: - self.app.inform.emit("[error_notcl] Failed to skew. No object selected") + self.app.inform.emit("[ERROR_NOTCL] Failed to skew. No object selected") # if type(self.solid_geometry) == list: # self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py)) @@ -2454,9 +2454,11 @@ class Gerber (Geometry): region = Polygon() else: region = Polygon(path) + if not region.is_valid: if not follow: region = region.buffer(0, int(self.steps_per_circle / 4)) + if not region.is_empty: poly_buffer.append(region) @@ -2531,8 +2533,8 @@ class Gerber (Geometry): pass last_path_aperture = current_aperture else: - self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline)) - self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!") + self.app.inform.emit("[WARNING] Coordinates missing, line ignored: %s" % str(gline)) + self.app.inform.emit("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!") elif current_operation_code == 2: if len(path) > 1: @@ -2550,7 +2552,7 @@ class Gerber (Geometry): geo = Polygon(path) except ValueError: log.warning("Problem %s %s" % (gline, line_num)) - self.app.inform.emit("[error] Region does not have enough points. " + self.app.inform.emit("[ERROR] Region does not have enough points. " "File will be processed but there are parser errors. " "Line number: %s" % str(line_num)) else: @@ -2574,8 +2576,8 @@ class Gerber (Geometry): if linear_x is not None and linear_y is not None: path = [[linear_x, linear_y]] # Start new path else: - self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline)) - self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!") + self.app.inform.emit("[WARNING] Coordinates missing, line ignored: %s" % str(gline)) + self.app.inform.emit("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!") # Flash # Not allowed in region mode. @@ -2838,6 +2840,7 @@ class Gerber (Geometry): 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) @@ -2857,8 +2860,9 @@ class Gerber (Geometry): traceback.print_tb(tb) #print traceback.format_exc() - log.error("PARSING FAILED. Line %d: %s" % (line_num, gline)) - self.app.inform.emit("[error] Gerber Parser ERROR.\n Line %d: %s" % (line_num, gline), repr(err)) + log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline)) + loc = 'Gerber Line #%d Gerber Line Content: %s\n' % (line_num, gline) + repr(err) + self.app.inform.emit("[ERROR]Gerber Parser ERROR.\n%s:" % loc) @staticmethod def create_flash_geometry(location, aperture, steps_per_circle=None): @@ -3035,7 +3039,7 @@ class Gerber (Geometry): try: xfactor = float(xfactor) except: - self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.") + self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.") return if yfactor is None: @@ -3044,7 +3048,7 @@ class Gerber (Geometry): try: yfactor = float(yfactor) except: - self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.") + self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.") return if point is None: @@ -3096,7 +3100,7 @@ class Gerber (Geometry): try: dx, dy = vect except TypeError: - self.app.inform.emit("[error_notcl]An (x,y) pair of values are needed. " + self.app.inform.emit("[ERROR_NOTCL]An (x,y) pair of values are needed. " "Probable you entered only one value in the Offset field.") return @@ -3460,7 +3464,7 @@ class Excellon(Geometry): # and we need to exit from here if self.detect_gcode_re.search(eline): log.warning("This is GCODE mark: %s" % eline) - self.app.inform.emit('[error_notcl] This is GCODE mark: %s' % eline) + self.app.inform.emit('[ERROR_NOTCL] This is GCODE mark: %s' % eline) return # Header Begin (M48) # @@ -3987,10 +3991,13 @@ class Excellon(Geometry): # from self.defaults['excellon_units'] log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) - except Exception as e: - log.error("PARSING FAILED. Line %d: %s" % (line_num, eline)) - self.app.inform.emit('[error] Excellon Parser ERROR.\nPARSING FAILED. Line %d: %s' % (line_num, eline)) + log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline)) + msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n" + msg += '[ERROR] Excellon Parser error.\nParsing Failed. Line %d: %s\n' % (line_num, eline) + msg += traceback.format_exc() + self.app.inform.emit(msg) + return "fail" def parse_number(self, number_str): @@ -4059,7 +4066,7 @@ class Excellon(Geometry): for drill in self.drills: # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0) if drill['tool'] is '': - self.app.inform.emit("[warning] Excellon.create_geometry() -> a drill location was skipped " + self.app.inform.emit("[WARNING] Excellon.create_geometry() -> a drill location was skipped " "due of not having a tool associated.\n" "Check the resulting GCode.") log.debug("Excellon.create_geometry() -> a drill location was skipped " @@ -4363,11 +4370,11 @@ class CNCjob(Geometry): def __init__(self, units="in", kind="generic", tooldia=0.0, z_cut=-0.002, z_move=0.1, - feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0, + feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0, feedrate_probe=3.0, pp_geometry_name='default', pp_excellon_name='default', - depthpercut = 0.1, + depthpercut=0.1,z_pdepth=-0.02, spindlespeed=None, dwell=True, dwelltime=1000, - toolchangez=0.787402, + toolchangez=0.787402, toolchange_xy=[0.0, 0.0], endz=2.0, segx=None, segy=None, @@ -4392,7 +4399,8 @@ class CNCjob(Geometry): self.tooldia = tooldia self.toolchangez = toolchangez - self.toolchange_xy = None + self.toolchange_xy = toolchange_xy + self.toolchange_xy_type = None self.endz = endz self.depthpercut = depthpercut @@ -4411,6 +4419,15 @@ class CNCjob(Geometry): self.pp_excellon_name = pp_excellon_name self.pp_excellon = self.app.postprocessors[self.pp_excellon_name] + # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1 + self.f_plunge = None + + # how much depth the probe can probe before error + self.z_pdepth = z_pdepth if z_pdepth else None + + # the feedrate(speed) with which the probel travel while probing + self.feedrate_probe = feedrate_probe if feedrate_probe else None + self.spindlespeed = spindlespeed self.dwell = dwell self.dwelltime = dwelltime @@ -4420,6 +4437,9 @@ class CNCjob(Geometry): self.input_geometry_bounds = None + self.oldx = None + self.oldy = None + # Attributes to be included in serialization # Always append to it because it carries contents # from Geometry. @@ -4490,7 +4510,7 @@ class CNCjob(Geometry): return path def generate_from_excellon_by_tool(self, exobj, tools="all", drillz = 3.0, - toolchange=False, toolchangez=0.1, toolchangexy="0.0, 0.0", + toolchange=False, toolchangez=0.1, toolchangexy='', endz=2.0, startz=None, excellon_optimization_type='B'): """ @@ -4520,25 +4540,40 @@ class CNCjob(Geometry): :rtype: None """ if drillz > 0: - self.app.inform.emit("[warning] The Cut Z parameter has positive value. " + self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. " "It is the depth value to drill into material.\n" "The Cut Z parameter needs to have a negative value, assuming it is a typo " "therefore the app will convert the value to negative. " "Check the resulting CNC code (Gcode etc).") self.z_cut = -drillz elif drillz == 0: - self.app.inform.emit("[warning] The Cut Z parameter is zero. " + self.app.inform.emit("[WARNING] The Cut Z parameter is zero. " "There will be no cut, skipping %s file" % exobj.options['name']) return else: self.z_cut = drillz self.toolchangez = toolchangez - self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + + try: + if toolchangexy == '': + self.toolchange_xy = None + else: + self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + if len(self.toolchange_xy) < 2: + self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be " + "in the format (x, y) \nbut now there is only one value, not two. ") + return 'fail' + except Exception as e: + log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e)) + pass self.startz = startz self.endz = endz + self.pp_excellon = self.app.postprocessors[self.pp_excellon_name] + p = self.pp_excellon + log.debug("Creating CNC Job from Excellon...") # Tools @@ -4576,15 +4611,19 @@ class CNCjob(Geometry): self.gcode = [] - # Basic G-Code macros - self.pp_excellon = self.app.postprocessors[self.pp_excellon_name] - p = self.pp_excellon + self.f_plunge = self.app.defaults["excellon_f_plunge"] # Initialization gcode = self.doformat(p.start_code) gcode += self.doformat(p.feedrate_code) - gcode += self.doformat(p.lift_code, x=0, y=0) - gcode += self.doformat(p.startz_code, x=0, y=0) + + if toolchange is False: + if self.toolchange_xy is not None: + gcode += self.doformat(p.lift_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) + gcode += self.doformat(p.startz_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) + else: + gcode += self.doformat(p.lift_code, x=0.0, y=0.0) + gcode += self.doformat(p.startz_code, x=0.0, y=0.0) # Distance callback class CreateDistanceCallback(object): @@ -4618,8 +4657,13 @@ class CNCjob(Geometry): locations.append((point.coords.xy[0][0], point.coords.xy[1][0])) return locations - oldx = 0 - oldy = 0 + if self.toolchange_xy is not None: + self.oldx = self.toolchange_xy[0] + self.oldy = self.toolchange_xy[1] + else: + self.oldx = 0.0 + self.oldy = 0.0 + measured_distance = 0 current_platform = platform.architecture()[0] @@ -4684,7 +4728,7 @@ class CNCjob(Geometry): if tool in points: # Tool change sequence (optional) if toolchange: - gcode += self.doformat(p.toolchange_code,toolchangexy=(oldx, oldy)) + gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy)) gcode += self.doformat(p.spindle_code) # Spindle start if self.dwell is True: gcode += self.doformat(p.dwell_code) # Dwell time @@ -4702,9 +4746,9 @@ class CNCjob(Geometry): gcode += self.doformat(p.down_code, x=locx, y=locy) gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy) gcode += self.doformat(p.lift_code, x=locx, y=locy) - measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy)) - oldx = locx - oldy = locy + measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy)) + self.oldx = locx + self.oldy = locy log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance)) elif excellon_optimization_type == 'B': log.debug("Using OR-Tools Basic drill path optimization.") @@ -4758,7 +4802,7 @@ class CNCjob(Geometry): if tool in points: # Tool change sequence (optional) if toolchange: - gcode += self.doformat(p.toolchange_code,toolchangexy=(oldx, oldy)) + gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy)) gcode += self.doformat(p.spindle_code) # Spindle start) if self.dwell is True: gcode += self.doformat(p.dwell_code) # Dwell time @@ -4775,12 +4819,12 @@ class CNCjob(Geometry): gcode += self.doformat(p.down_code, x=locx, y=locy) gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy) gcode += self.doformat(p.lift_code, x=locx, y=locy) - measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy)) - oldx = locx - oldy = locy + measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy)) + self.oldx = locx + self.oldy = locy log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance)) else: - self.app.inform.emit("[error_notcl] Wrong optimization type selected.") + self.app.inform.emit("[ERROR_NOTCL] Wrong optimization type selected.") return else: log.debug("Using Travelling Salesman drill path optimization.") @@ -4792,7 +4836,7 @@ class CNCjob(Geometry): if tool in points: # Tool change sequence (optional) if toolchange: - gcode += self.doformat(p.toolchange_code, toolchangexy=(oldx, oldy)) + gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy)) gcode += self.doformat(p.spindle_code) # Spindle start) if self.dwell is True: gcode += self.doformat(p.dwell_code) # Dwell time @@ -4811,15 +4855,15 @@ class CNCjob(Geometry): gcode += self.doformat(p.down_code, x=point[0], y=point[1]) gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1]) gcode += self.doformat(p.lift_code, x=point[0], y=point[1]) - measured_distance += abs(distance_euclidian(point[0], point[1], oldx, oldy)) - oldx = point[0] - oldy = point[1] + measured_distance += abs(distance_euclidian(point[0], point[1], self.oldx, self.oldy)) + self.oldx = point[0] + self.oldy = point[1] log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance)) gcode += self.doformat(p.spindle_stop_code) # Spindle stop gcode += self.doformat(p.end_code, x=0, y=0) - measured_distance += abs(distance_euclidian(oldx, oldy, 0, 0)) + measured_distance += abs(distance_euclidian(self.oldx, self.oldy, 0, 0)) log.debug("The total travel distance including travel to end position is: %s" % str(measured_distance) + '\n') self.gcode = gcode @@ -4887,19 +4931,32 @@ class CNCjob(Geometry): self.multidepth = multidepth self.toolchangez = toolchangez - self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + + try: + if toolchangexy == '': + self.toolchange_xy = None + else: + self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + if len(self.toolchange_xy) < 2: + self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be " + "in the format (x, y) \nbut now there is only one value, not two. ") + return 'fail' + except Exception as e: + log.debug("camlib.CNCJob.generate_from_multitool_geometry() --> %s" % str(e)) + pass self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default' + self.f_plunge = self.app.defaults["geometry_f_plunge"] if self.z_cut > 0: - self.app.inform.emit("[warning] The Cut Z parameter has positive value. " + self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. " "It is the depth value to cut into material.\n" "The Cut Z parameter needs to have a negative value, assuming it is a typo " "therefore the app will convert the value to negative." "Check the resulting CNC code (Gcode etc).") self.z_cut = -self.z_cut elif self.z_cut == 0: - self.app.inform.emit("[warning] The Cut Z parameter is zero. " + self.app.inform.emit("[WARNING] The Cut Z parameter is zero. " "There will be no cut, skipping %s file" % self.options['name']) ## Index first and last points in paths @@ -4936,14 +4993,17 @@ class CNCjob(Geometry): self.gcode = self.doformat(p.start_code) self.gcode += self.doformat(p.feedrate_code) # sets the feed rate - self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height - self.gcode += self.doformat(p.startz_code, x=0, y=0) + + if toolchange is False: + self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height + self.gcode += self.doformat(p.startz_code, x=0, y=0) if toolchange: - if "line_xyz" in self.pp_geometry_name: - self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) - else: - self.gcode += self.doformat(p.toolchange_code) + # if "line_xyz" in self.pp_geometry_name: + # self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) + # else: + # self.gcode += self.doformat(p.toolchange_code) + self.gcode += self.doformat(p.toolchange_code) self.gcode += self.doformat(p.spindle_code) # Spindle start if self.dwell is True: @@ -5023,13 +5083,13 @@ class CNCjob(Geometry): """ if not isinstance(geometry, Geometry): - self.app.inform.emit("[error]Expected a Geometry, got %s" % type(geometry)) + self.app.inform.emit("[ERROR]Expected a Geometry, got %s" % type(geometry)) return 'fail' log.debug("Generate_from_geometry_2()") # if solid_geometry is empty raise an exception if not geometry.solid_geometry: - self.app.inform.emit("[error_notcl]Trying to generate a CNC Job " + self.app.inform.emit("[ERROR_NOTCL]Trying to generate a CNC Job " "from a Geometry object without solid_geometry.") temp_solid_geometry = [] @@ -5067,19 +5127,32 @@ class CNCjob(Geometry): self.multidepth = multidepth self.toolchangez = toolchangez - self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + + try: + if toolchangexy == '': + self.toolchange_xy = None + else: + self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")] + if len(self.toolchange_xy) < 2: + self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be " + "in the format (x, y) \nbut now there is only one value, not two. ") + return 'fail' + except Exception as e: + log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e)) + pass self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default' + self.f_plunge = self.app.defaults["geometry_f_plunge"] if self.z_cut > 0: - self.app.inform.emit("[warning] The Cut Z parameter has positive value. " + self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. " "It is the depth value to cut into material.\n" "The Cut Z parameter needs to have a negative value, assuming it is a typo " "therefore the app will convert the value to negative." "Check the resulting CNC code (Gcode etc).") self.z_cut = -self.z_cut elif self.z_cut == 0: - self.app.inform.emit("[warning] The Cut Z parameter is zero. " + self.app.inform.emit("[WARNING] The Cut Z parameter is zero. " "There will be no cut, skipping %s file" % geometry.options['name']) ## Index first and last points in paths @@ -5113,18 +5186,23 @@ class CNCjob(Geometry): self.pp_geometry = self.app.postprocessors[self.pp_geometry_name] p = self.pp_geometry + self.oldx = 0.0 + self.oldy = 0.0 + self.gcode = self.doformat(p.start_code) self.gcode += self.doformat(p.feedrate_code) # sets the feed rate - self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height - self.gcode += self.doformat(p.startz_code, x=0, y=0) + if toolchange is False: + self.gcode += self.doformat(p.lift_code, x=self.oldx , y=self.oldy ) # Move (up) to travel height + self.gcode += self.doformat(p.startz_code, x=self.oldx , y=self.oldy ) if toolchange: - if "line_xyz" in self.pp_geometry_name: - self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) - else: - self.gcode += self.doformat(p.toolchange_code) + # if "line_xyz" in self.pp_geometry_name: + # self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1]) + # else: + # self.gcode += self.doformat(p.toolchange_code) + self.gcode += self.doformat(p.toolchange_code) self.gcode += self.doformat(p.spindle_code) # Spindle start @@ -5328,10 +5406,17 @@ class CNCjob(Geometry): # Current path: temporary storage until tool is # lifted or lowered. - if self.toolchange_xy == "excellon": - pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")] + if self.toolchange_xy_type == "excellon": + if self.app.defaults["excellon_toolchangexy"] == '': + pos_xy = [0, 0] + else: + pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")] else: - pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")] + if self.app.defaults["geometry_toolchangexy"] == '': + pos_xy = [0, 0] + else: + pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")] + path = [pos_xy] # path = [(0, 0)] @@ -5456,7 +5541,7 @@ class CNCjob(Geometry): def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None, color={"T": ["#F0E24D4C", "#B5AB3A4C"], "C": ["#5E6CFFFF", "#4650BDFF"]}, - alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False): + alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False, kind='all'): """ Plots the G-code job onto the given axes. @@ -5477,7 +5562,15 @@ class CNCjob(Geometry): if tooldia == 0: for geo in gcode_parsed: - obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible) + if kind == 'all': + obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible) + elif kind == 'travel': + if geo['kind'][0] == 'T': + obj.add_shape(shape=geo['geom'], color=color['T'][1], visible=visible) + elif kind == 'cut': + if geo['kind'][0] == 'C': + obj.add_shape(shape=geo['geom'], color=color['C'][1], visible=visible) + else: text = [] pos = [] @@ -5488,8 +5581,17 @@ class CNCjob(Geometry): pos.append(geo['geom'].coords[0]) poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance) - obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0], + if kind == 'all': + obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0], visible=visible, layer=1 if geo['kind'][0] == 'C' else 2) + elif kind == 'travel': + if geo['kind'][0] == 'T': + obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0], + visible=visible, layer=2) + elif kind == 'cut': + if geo['kind'][0] == 'C': + obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0], + visible=visible, layer=1) obj.annotation.set(text=text, pos=pos, visible=obj.options['plot']) @@ -5798,6 +5900,7 @@ class CNCjob(Geometry): else: # it's a Shapely object, return it's bounds return obj.bounds + if self.multitool is False: log.debug("CNCJob->bounds()") if self.solid_geometry is None: @@ -5806,21 +5909,30 @@ class CNCjob(Geometry): bounds_coords = bounds_rec(self.solid_geometry) else: + for k, v in self.cnc_tools.items(): minx = Inf miny = Inf maxx = -Inf maxy = -Inf - - for k in v['solid_geometry']: - minx_, miny_, maxx_, maxy_ = bounds_rec(k) + try: + for k in v['solid_geometry']: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + except TypeError: + minx_, miny_, maxx_, maxy_ = bounds_rec(v['solid_geometry']) minx = min(minx, minx_) miny = min(miny, miny_) maxx = max(maxx, maxx_) maxy = max(maxy, maxy_) + bounds_coords = minx, miny, maxx, maxy return bounds_coords + # TODO This function should be replaced at some point with a "real" function. Until then it's an ugly hack ... def scale(self, xfactor, yfactor=None, point=None): """ Scales all the geometry on the XY plane in the object by the @@ -5844,8 +5956,124 @@ class CNCjob(Geometry): else: px, py = point - for g in self.gcode_parsed: - g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py)) + def scale_g(g): + """ + + :param g: 'g' parameter it's a gcode string + :return: scaled gcode string + """ + + temp_gcode = '' + header_start = False + header_stop = False + units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() + + lines = StringIO(g) + for line in lines: + + # this changes the GCODE header ---- UGLY HACK + if "TOOL DIAMETER" in line or "Feedrate:" in line: + header_start = True + + if "G20" in line or "G21" in line: + header_start = False + header_stop = True + + if header_start is True: + header_stop = False + if "in" in line: + if units == 'MM': + line = line.replace("in", "mm") + if "mm" in line: + if units == 'IN': + line = line.replace("mm", "in") + + # find any float number in header (even multiple on the same line) and convert it + numbers_in_header = re.findall(self.g_nr_re, line) + if numbers_in_header: + for nr in numbers_in_header: + new_nr = float(nr) * xfactor + # replace the updated string + line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr)) + ) + + # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message + if header_stop is True: + if "G20" in line: + if units == 'MM': + line = line.replace("G20", "G21") + if "G21" in line: + if units == 'IN': + line = line.replace("G21", "G20") + + # find the X group + match_x = self.g_x_re.search(line) + if match_x: + if match_x.group(1) is not None: + new_x = float(match_x.group(1)[1:]) * xfactor + # replace the updated string + line = line.replace( + match_x.group(1), + 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x) + ) + # find the Y group + match_y = self.g_y_re.search(line) + if match_y: + if match_y.group(1) is not None: + new_y = float(match_y.group(1)[1:]) * yfactor + line = line.replace( + match_y.group(1), + 'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y) + ) + # find the Z group + match_z = self.g_z_re.search(line) + if match_z: + if match_z.group(1) is not None: + new_z = float(match_z.group(1)[1:]) * xfactor + line = line.replace( + match_z.group(1), + 'Z%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_z) + ) + + # find the F group + match_f = self.g_f_re.search(line) + if match_f: + if match_f.group(1) is not None: + new_f = float(match_f.group(1)[1:]) * xfactor + line = line.replace( + match_f.group(1), + 'F%.*f' % (self.app.defaults["cncjob_fr_decimals"], new_f) + ) + # find the T group (tool dia on toolchange) + match_t = self.g_t_re.search(line) + if match_t: + if match_t.group(1) is not None: + new_t = float(match_t.group(1)[1:]) * xfactor + line = line.replace( + match_t.group(1), + '= %.*f' % (self.app.defaults["cncjob_coords_decimals"], new_t) + ) + + temp_gcode += line + lines.close() + header_stop = False + return temp_gcode + + if self.multitool is False: + # offset Gcode + self.gcode = scale_g(self.gcode) + # offset geometry + for g in self.gcode_parsed: + g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py)) + self.create_geometry() + else: + for k, v in self.cnc_tools.items(): + # scale Gcode + v['gcode'] = scale_g(v['gcode']) + # scale gcode_parsed + for g in v['gcode_parsed']: + g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py)) + v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']]) self.create_geometry() @@ -5875,7 +6103,7 @@ class CNCjob(Geometry): lines = StringIO(g) for line in lines: # find the X group - match_x = self.g_offsetx_re.search(line) + match_x = self.g_x_re.search(line) if match_x: if match_x.group(1) is not None: # get the coordinate and add X offset @@ -5885,7 +6113,7 @@ class CNCjob(Geometry): match_x.group(1), 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x) ) - match_y = self.g_offsety_re.search(line) + match_y = self.g_y_re.search(line) if match_y: if match_y.group(1) is not None: new_y = float(match_y.group(1)[1:]) + dy diff --git a/flatcamTools/ToolCalculators.py b/flatcamTools/ToolCalculators.py index b937a2e0..cd42881e 100644 --- a/flatcamTools/ToolCalculators.py +++ b/flatcamTools/ToolCalculators.py @@ -10,6 +10,7 @@ class ToolCalculator(FlatCAMTool): toolName = "Calculators" v_shapeName = "V-Shape Tool Calculator" unitsName = "Units Calculator" + eplateName = "ElectroPlating Calculator" def __init__(self, app): FlatCAMTool.__init__(self, app) @@ -20,7 +21,9 @@ class ToolCalculator(FlatCAMTool): title_label = QtWidgets.QLabel("%s" % self.toolName) self.layout.addWidget(title_label) - ## V-shape Tool Calculator + ############################ + ## V-shape Tool Calculator ## + ############################ self.v_shape_spacer_label = QtWidgets.QLabel(" ") self.layout.addWidget(self.v_shape_spacer_label) @@ -35,30 +38,30 @@ class ToolCalculator(FlatCAMTool): self.tipDia_label = QtWidgets.QLabel("Tip Diameter:") self.tipDia_entry = FCEntry() - self.tipDia_entry.setFixedWidth(70) + # self.tipDia_entry.setFixedWidth(70) self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.tipDia_entry.setToolTip('This is the diameter of the tool tip.\n' + self.tipDia_label.setToolTip('This is the diameter of the tool tip.\n' 'The manufacturer specifies it.') self.tipAngle_label = QtWidgets.QLabel("Tip Angle:") self.tipAngle_entry = FCEntry() - self.tipAngle_entry.setFixedWidth(70) + # self.tipAngle_entry.setFixedWidth(70) self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.tipAngle_entry.setToolTip("This is the angle of the tip of the tool.\n" + self.tipAngle_label.setToolTip("This is the angle of the tip of the tool.\n" "It is specified by manufacturer.") self.cutDepth_label = QtWidgets.QLabel("Cut Z:") self.cutDepth_entry = FCEntry() - self.cutDepth_entry.setFixedWidth(70) + # self.cutDepth_entry.setFixedWidth(70) self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.cutDepth_entry.setToolTip("This is the depth to cut into the material.\n" + self.cutDepth_label.setToolTip("This is the depth to cut into the material.\n" "In the CNCJob is the CutZ parameter.") self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:") self.effectiveToolDia_entry = FCEntry() - self.effectiveToolDia_entry.setFixedWidth(70) + # self.effectiveToolDia_entry.setFixedWidth(70) self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.effectiveToolDia_entry.setToolTip("This is the tool diameter to be entered into\n" + self.effectiveToolDia_label.setToolTip("This is the tool diameter to be entered into\n" "FlatCAM Gerber section.\n" "In the CNCJob section it is called >Tool dia<.") # self.effectiveToolDia_entry.setEnabled(False) @@ -69,19 +72,21 @@ class ToolCalculator(FlatCAMTool): form_layout.addRow(self.cutDepth_label, self.cutDepth_entry) form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry) - ## Buttons - self.calculate_button = QtWidgets.QPushButton("Calculate") - self.calculate_button.setFixedWidth(70) - self.calculate_button.setToolTip( + self.calculate_vshape_button = QtWidgets.QPushButton("Calculate") + # self.calculate_button.setFixedWidth(70) + self.calculate_vshape_button.setToolTip( "Calculate either the Cut Z or the effective tool diameter,\n " "depending on which is desired and which is known. " ) self.empty_label = QtWidgets.QLabel(" ") - form_layout.addRow(self.empty_label, self.calculate_button) + form_layout.addRow(self.empty_label, self.calculate_vshape_button) + + ###################### + ## Units Calculator ## + ###################### - ## Units Calculator self.unists_spacer_label = QtWidgets.QLabel(" ") self.layout.addWidget(self.unists_spacer_label) @@ -89,25 +94,109 @@ class ToolCalculator(FlatCAMTool): units_label = QtWidgets.QLabel("%s" % self.unitsName) self.layout.addWidget(units_label) - #Form Layout - form_units_layout = QtWidgets.QFormLayout() - self.layout.addLayout(form_units_layout) + #Grid Layout + grid_units_layout = QtWidgets.QGridLayout() + self.layout.addLayout(grid_units_layout) inch_label = QtWidgets.QLabel("INCH") mm_label = QtWidgets.QLabel("MM") + grid_units_layout.addWidget(mm_label, 0, 0) + grid_units_layout.addWidget( inch_label, 0, 1) self.inch_entry = FCEntry() - self.inch_entry.setFixedWidth(70) + # self.inch_entry.setFixedWidth(70) self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM") self.mm_entry = FCEntry() - self.mm_entry.setFixedWidth(70) + # self.mm_entry.setFixedWidth(130) self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH") - form_units_layout.addRow(mm_label, inch_label) - form_units_layout.addRow(self.mm_entry, self.inch_entry) + grid_units_layout.addWidget(self.mm_entry, 1, 0) + grid_units_layout.addWidget(self.inch_entry, 1, 1) + + #################################### + ## ElectroPlating Tool Calculator ## + #################################### + + self.plate_spacer_label = QtWidgets.QLabel(" ") + self.layout.addWidget(self.plate_spacer_label) + + ## Title of the ElectroPlating Tools Calculator + plate_title_label = QtWidgets.QLabel("%s" % self.eplateName) + plate_title_label.setToolTip( + "This calculator is useful for those who plate the via/pad/drill holes,\n" + "using a method like grahite ink or calcium hypophosphite ink or palladium chloride." + ) + self.layout.addWidget(plate_title_label) + + ## Plate Form Layout + plate_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(plate_form_layout) + + self.pcblengthlabel = QtWidgets.QLabel("Board Length:") + self.pcblength_entry = FCEntry() + # self.pcblengthlabel.setFixedWidth(70) + self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.pcblengthlabel.setToolTip('This is the board length. In centimeters.') + + self.pcbwidthlabel = QtWidgets.QLabel("Board Width:") + self.pcbwidth_entry = FCEntry() + # self.pcbwidthlabel.setFixedWidth(70) + self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.pcbwidthlabel.setToolTip('This is the board width.In centimeters.') + + self.cdensity_label = QtWidgets.QLabel("Current Density:") + self.cdensity_entry = FCEntry() + # self.cdensity_entry.setFixedWidth(70) + self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.cdensity_label.setToolTip("Current density to pass through the board. \n" + "In Amps per Square Feet ASF.") + + + self.growth_label = QtWidgets.QLabel("Copper Growth:") + self.growth_entry = FCEntry() + # self.growth_entry.setFixedWidth(70) + self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.growth_label.setToolTip("How thick the copper growth is intended to be.\n" + "In microns.") + + # self.growth_entry.setEnabled(False) + + self.cvaluelabel = QtWidgets.QLabel("Current Value:") + self.cvalue_entry = FCEntry() + # self.cvaluelabel.setFixedWidth(70) + self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.cvaluelabel.setToolTip('This is the current intensity value\n' + 'to be set on the Power Supply. In Amps.') + self.cvalue_entry.setDisabled(True) + + self.timelabel = QtWidgets.QLabel("Time:") + self.time_entry = FCEntry() + # self.timelabel.setFixedWidth(70) + self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.timelabel.setToolTip('This is the calculated time required for the procedure.\n' + 'In minutes.') + self.time_entry.setDisabled(True) + + plate_form_layout.addRow(self.pcblengthlabel, self.pcblength_entry) + plate_form_layout.addRow(self.pcbwidthlabel, self.pcbwidth_entry) + plate_form_layout.addRow(self.cdensity_label, self.cdensity_entry) + plate_form_layout.addRow(self.growth_label, self.growth_entry) + plate_form_layout.addRow(self.cvaluelabel, self.cvalue_entry) + plate_form_layout.addRow(self.timelabel, self.time_entry) + + ## Buttons + self.calculate_plate_button = QtWidgets.QPushButton("Calculate") + # self.calculate_button.setFixedWidth(70) + self.calculate_plate_button.setToolTip( + "Calculate the current intensity value and the procedure time,\n " + "depending on the parameters above" + ) + self.empty_label_2 = QtWidgets.QLabel(" ") + + plate_form_layout.addRow(self.empty_label_2, self.calculate_plate_button) self.layout.addStretch() @@ -116,13 +205,36 @@ class ToolCalculator(FlatCAMTool): self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia) self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia) self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia) - self.calculate_button.clicked.connect(self.on_calculate_tool_dia) + self.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia) self.mm_entry.editingFinished.connect(self.on_calculate_inch_units) self.inch_entry.editingFinished.connect(self.on_calculate_mm_units) + self.calculate_plate_button.clicked.connect(self.on_calculate_eplate) + + def run(self): + self.app.report_usage("ToolCalculators()") + + FlatCAMTool.run(self) + self.set_tool_ui() + self.app.ui.notebook.setTabText(2, "Calc. Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs) + + def set_tool_ui(self): ## Initialize form + self.mm_entry.set_value('0') + self.inch_entry.set_value('0') + + self.pcblength_entry.set_value('10') + self.pcbwidth_entry.set_value('10') + self.cdensity_entry.set_value('13') + self.growth_entry.set_value('10') + self.cvalue_entry.set_value(2.80) + self.time_entry.set_value(33.0) + if self.app.defaults["units"] == 'MM': self.tipDia_entry.set_value('0.2') self.tipAngle_entry.set_value('45') @@ -134,16 +246,6 @@ class ToolCalculator(FlatCAMTool): self.cutDepth_entry.set_value('9.84252') self.effectiveToolDia_entry.set_value('15.35433') - self.mm_entry.set_value('0') - self.inch_entry.set_value('0') - - def run(self): - FlatCAMTool.run(self) - self.app.ui.notebook.setTabText(2, "Calc. Tool") - - def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs) - def on_calculate_tool_dia(self): # Calculation: # Manufacturer gives total angle of the the tip but we need only half of it @@ -155,18 +257,117 @@ class ToolCalculator(FlatCAMTool): try: tip_diameter = float(self.tipDia_entry.get_value()) - half_tip_angle = float(self.tipAngle_entry.get_value()) / 2 + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tip_diameter = float(self.tipDia_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + half_tip_angle = float(self.tipAngle_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + half_tip_angle = float(self.tipAngle_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + half_tip_angle /= 2 + + try: cut_depth = float(self.cutDepth_entry.get_value()) - except: - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + cut_depth = float(self.cutDepth_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle))) self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter) def on_calculate_inch_units(self): - self.inch_entry.set_value('%.6f' % (float(self.mm_entry.get_value()) / 25.4)) + try: + mm_val = float(self.mm_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + mm_val = float(self.mm_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + self.inch_entry.set_value('%.6f' % (mm_val / 25.4)) def on_calculate_mm_units(self): - self.mm_entry.set_value('%.6f' % (float(self.inch_entry.get_value()) * 25.4)) + try: + inch_val = float(self.inch_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + inch_val = float(self.inch_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + self.mm_entry.set_value('%.6f' % (inch_val * 25.4)) + + def on_calculate_eplate(self): + + try: + length = float(self.pcblength_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + length = float(self.pcblength_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + width = float(self.pcbwidth_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + width = float(self.pcbwidth_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + density = float(self.cdensity_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + density = float(self.cdensity_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + copper = float(self.growth_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + copper = float(self.growth_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + calculated_current = (length * width * density) * 0.0021527820833419 + calculated_time = copper * 2.142857142857143 * float(20 / density) + + self.cvalue_entry.set_value('%.2f' % calculated_current) + self.time_entry.set_value('%.1f' % calculated_time) # end of file \ No newline at end of file diff --git a/flatcamTools/ToolCutout.py b/flatcamTools/ToolCutOut.py similarity index 80% rename from flatcamTools/ToolCutout.py rename to flatcamTools/ToolCutOut.py index 4d2e7dbd..6176808d 100644 --- a/flatcamTools/ToolCutout.py +++ b/flatcamTools/ToolCutOut.py @@ -7,7 +7,8 @@ from GUIElements import IntEntry, RadioSet, LengthEntry from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber -class ToolCutout(FlatCAMTool): + +class ToolCutOut(FlatCAMTool): toolName = "Cutout PCB" @@ -48,6 +49,7 @@ class ToolCutout(FlatCAMTool): self.obj_combo.setModel(self.app.collection) self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.obj_combo.setCurrentIndex(1) + self.object_label = QtWidgets.QLabel("Object:") self.object_label.setToolTip( "Object to be cutout. " @@ -172,11 +174,11 @@ class ToolCutout(FlatCAMTool): self.layout.addStretch() ## Init GUI - self.dia.set_value(1) - self.margin.set_value(0) - self.gapsize.set_value(1) - self.gaps.set_value(4) - self.gaps_rect_radio.set_value("4") + # self.dia.set_value(1) + # self.margin.set_value(0) + # self.gapsize.set_value(1) + # self.gaps.set_value(4) + # self.gaps_rect_radio.set_value("4") ## Signals self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout) @@ -190,14 +192,18 @@ class ToolCutout(FlatCAMTool): self.obj_combo.setCurrentIndex(0) def run(self): + self.app.report_usage("ToolCutOut()") + FlatCAMTool.run(self) - self.set_ui() + self.set_tool_ui() self.app.ui.notebook.setTabText(2, "Cutout Tool") def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs) - def set_ui(self): + def set_tool_ui(self): + self.reset_fields() + self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"])) self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"])) self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"])) @@ -216,45 +222,63 @@ class ToolCutout(FlatCAMTool): try: cutout_obj = self.app.collection.get_by_name(str(name)) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name) return "Could not retrieve object: %s" % name if cutout_obj is None: - self.app.inform.emit("[error_notcl]There is no object selected for Cutout.\nSelect one and try again.") + self.app.inform.emit("[ERROR_NOTCL]There is no object selected for Cutout.\nSelect one and try again.") return try: dia = float(self.dia.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + dia = float(self.dia.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + try: margin = float(self.margin.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.margin.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. " + "Add it and retry.") + return + try: gapsize = float(self.gapsize.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + gapsize = float(self.gapsize.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. " + "Add it and retry.") + return + try: gaps = self.gaps.get_value() except TypeError: - self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.") + self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.") return if 0 in {dia}: - self.app.inform.emit("[warning_notcl]Tool Diameter is zero value. Change it to a positive integer.") + self.app.inform.emit("[WARNING_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") return "Tool Diameter is zero value. Change it to a positive integer." if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']: - self.app.inform.emit("[warning_notcl] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " + self.app.inform.emit("[WARNING_NOTCL] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " "Fill in a correct value and retry. ") return if cutout_obj.multigeo is True: - self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n" + self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n" "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n" "and after that perform Cutout.") return @@ -338,39 +362,57 @@ class ToolCutout(FlatCAMTool): try: cutout_obj = self.app.collection.get_by_name(str(name)) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name) return "Could not retrieve object: %s" % name if cutout_obj is None: - self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj) + self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % cutout_obj) try: dia = float(self.dia.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + dia = float(self.dia.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + try: margin = float(self.margin.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.margin.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. " + "Add it and retry.") + return + try: gapsize = float(self.gapsize.get_value()) - except TypeError: - self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + gapsize = float(self.gapsize.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. " + "Add it and retry.") + return + try: gaps = self.gaps_rect_radio.get_value() except TypeError: - self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.") + self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.") return if 0 in {dia}: - self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.") + self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") return "Tool Diameter is zero value. Change it to a positive integer." if cutout_obj.multigeo is True: - self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n" + self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n" "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n" "and after that perform Cutout.") return diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 475c3db3..3223616e 100644 --- a/flatcamTools/ToolDblSided.py +++ b/flatcamTools/ToolDblSided.py @@ -6,6 +6,7 @@ from shapely.geometry import Point from shapely import affinity from PyQt5 import QtCore + class DblSidedTool(FlatCAMTool): toolName = "2-Sided PCB" @@ -115,8 +116,8 @@ class DblSidedTool(FlatCAMTool): self.axloc_label = QtWidgets.QLabel("Axis Ref:") self.axloc_label.setToolTip( "The axis should pass through a point or cut\n " - "a specified box (in a Geometry object) in \n" - "the middle." + "a specified box (in a FlatCAM object) through \n" + "the center." ) # grid_lay.addRow("Axis Location:", self.axis_location) grid_lay.addWidget(self.axloc_label, 8, 0) @@ -129,19 +130,18 @@ class DblSidedTool(FlatCAMTool): self.point_box_container = QtWidgets.QVBoxLayout() self.pb_label = QtWidgets.QLabel("Point/Box:") self.pb_label.setToolTip( - "Specify the point (x, y) through which the mirror axis \n " - "passes or the Geometry object containing a rectangle \n" - "that the mirror axis cuts in half." + "If 'Point' is selected above it store the coordinates (x, y) through which\n" + "the mirroring axis passes.\n" + "If 'Box' is selected above, select here a FlatCAM object (Gerber, Exc or Geo).\n" + "Through the center of this object pass the mirroring axis selected above." ) - # grid_lay.addRow("Point/Box:", self.point_box_container) self.add_point_button = QtWidgets.QPushButton("Add") self.add_point_button.setToolTip( - "Add the point (x, y) through which the mirror axis \n " - "passes or the Object containing a rectangle \n" - "that the mirror axis cuts in half.\n" - "The point is captured by pressing SHIFT key\n" - "and left mouse clicking on canvas or you can enter them manually." + "Add the coordinates in format (x, y) through which the mirroring axis \n " + "selected in 'MIRROR AXIS' pass.\n" + "The (x, y) coordinates are captured by pressing SHIFT key\n" + "and left mouse button click on canvas or you can enter the coords manually." ) self.add_point_button.setFixedWidth(40) @@ -173,9 +173,9 @@ class DblSidedTool(FlatCAMTool): self.ah_label.setToolTip( "Alignment holes (x1, y1), (x2, y2), ... " "on one side of the mirror axis. For each set of (x, y) coordinates\n" - "entered here, a pair of drills will be created: one on the\n" - "coordinates entered and one in mirror position over the axis\n" - "selected above in the 'Mirror Axis'." + "entered here, a pair of drills will be created:\n\n" + "- one drill at the coordinates from the field\n" + "- one drill in mirror position over the axis selected above in the 'Mirror Axis'." ) self.layout.addWidget(self.ah_label) @@ -186,10 +186,13 @@ class DblSidedTool(FlatCAMTool): self.add_drill_point_button = QtWidgets.QPushButton("Add") self.add_drill_point_button.setToolTip( - "Add alignment drill holes coords (x1, y1), (x2, y2), ... \n" - "on one side of the mirror axis.\n" - "The point(s) can be captured by pressing SHIFT key\n" - "and left mouse clicking on canvas. Or you can enter them manually." + "Add alignment drill holes coords in the format: (x1, y1), (x2, y2), ... \n" + "on one side of the mirror axis.\n\n" + "The coordinates set can be obtained:\n" + "- press SHIFT key and left mouse clicking on canvas. Then click Add.\n" + "- press SHIFT key and left mouse clicking on canvas. Then CTRL+V in the field.\n" + "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n" + "- by entering the coords manually in the format: (x1, y1), (x2, y2), ..." ) self.add_drill_point_button.setFixedWidth(40) @@ -197,11 +200,10 @@ class DblSidedTool(FlatCAMTool): grid_lay1.addWidget(self.add_drill_point_button, 0, 3) ## Drill diameter for alignment holes - self.dt_label = QtWidgets.QLabel("Alignment Drill Creation:") + self.dt_label = QtWidgets.QLabel("Alignment Drill Diameter:") self.dt_label.setToolTip( - "Create a set of alignment drill holes\n" - "with the specified diameter,\n" - "at the specified coordinates." + "Diameter of the drill for the " + "alignment holes." ) self.layout.addWidget(self.dt_label) @@ -249,20 +251,19 @@ class DblSidedTool(FlatCAMTool): self.drill_values = "" - self.set_ui() - def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+D', **kwargs) def run(self): + self.app.report_usage("Tool2Sided()") + FlatCAMTool.run(self) - + self.set_tool_ui() self.app.ui.notebook.setTabText(2, "2-Sided Tool") - self.reset_fields() - self.set_ui() - def set_ui(self): - ## Initialize form + def set_tool_ui(self): + self.reset_fields() + self.point_entry.set_value("") self.alignment_holes.set_value("") @@ -283,7 +284,7 @@ class DblSidedTool(FlatCAMTool): try: px, py = self.point_entry.get_value() except TypeError: - self.app.inform.emit("[warning_notcl] 'Point' reference is selected and 'Point' coordinates " + self.app.inform.emit("[WARNING_NOTCL] 'Point' reference is selected and 'Point' coordinates " "are missing. Add them and retry.") return else: @@ -297,12 +298,15 @@ class DblSidedTool(FlatCAMTool): xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] dia = self.drill_dia.get_value() + if dia is None: + self.app.inform.emit("[WARNING_NOTCL]No value or wrong format in Drill Dia entry. Add it and retry.") + return tools = {"1": {"C": dia}} # holes = self.alignment_holes.get_value() holes = eval('[{}]'.format(self.alignment_holes.text())) if not holes: - self.app.inform.emit("[warning_notcl] There are no Alignment Drill Coordinates to use. Add them and retry.") + self.app.inform.emit("[WARNING_NOTCL] There are no Alignment Drill Coordinates to use. Add them and retry.") return drills = [] @@ -328,11 +332,11 @@ class DblSidedTool(FlatCAMTool): try: fcobj = model_index.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Gerber object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Gerber object loaded ...") return if not isinstance(fcobj, FlatCAMGerber): - self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.") + self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.") return axis = self.mirror_axis.get_value() @@ -342,7 +346,7 @@ class DblSidedTool(FlatCAMTool): try: px, py = self.point_entry.get_value() except TypeError: - self.app.inform.emit("[warning_notcl] 'Point' coordinates missing. " + self.app.inform.emit("[WARNING_NOTCL] 'Point' coordinates missing. " "Using Origin (0, 0) as mirroring reference.") px, py = (0, 0) @@ -352,7 +356,7 @@ class DblSidedTool(FlatCAMTool): try: bb_obj = model_index_box.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Box object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...") return xmin, ymin, xmax, ymax = bb_obj.bounds() @@ -370,11 +374,11 @@ class DblSidedTool(FlatCAMTool): try: fcobj = model_index.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Excellon object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Excellon object loaded ...") return if not isinstance(fcobj, FlatCAMExcellon): - self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.") + self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.") return axis = self.mirror_axis.get_value() @@ -388,7 +392,7 @@ class DblSidedTool(FlatCAMTool): try: bb_obj = model_index_box.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Box object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...") return xmin, ymin, xmax, ymax = bb_obj.bounds() @@ -406,11 +410,11 @@ class DblSidedTool(FlatCAMTool): try: fcobj = model_index.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Geometry object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Geometry object loaded ...") return if not isinstance(fcobj, FlatCAMGeometry): - self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.") + self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.") return axis = self.mirror_axis.get_value() @@ -424,7 +428,7 @@ class DblSidedTool(FlatCAMTool): try: bb_obj = model_index_box.internalPointer().obj except Exception as e: - self.app.inform.emit("[warning_notcl] There is no Box object loaded ...") + self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...") return xmin, ymin, xmax, ymax = bb_obj.bounds() diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index 94638ada..759a1580 100644 --- a/flatcamTools/ToolFilm.py +++ b/flatcamTools/ToolFilm.py @@ -1,6 +1,6 @@ from FlatCAMTool import FlatCAMTool -from GUIElements import RadioSet, FloatEntry +from GUIElements import RadioSet, FCEntry from PyQt5 import QtGui, QtCore, QtWidgets @@ -44,6 +44,7 @@ class Film(FlatCAMTool): self.tf_object_combo.setModel(self.app.collection) self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.tf_object_combo.setCurrentIndex(1) + self.tf_object_label = QtWidgets.QLabel("Film Object:") self.tf_object_label.setToolTip( "Object for which to create the film." @@ -101,7 +102,7 @@ class Film(FlatCAMTool): # Boundary for negative film generation - self.boundary_entry = FloatEntry() + self.boundary_entry = FCEntry() self.boundary_label = QtWidgets.QLabel("Border:") self.boundary_label.setToolTip( "Specify a border around the object.\n" @@ -115,6 +116,15 @@ class Film(FlatCAMTool): ) tf_form_layout.addRow(self.boundary_label, self.boundary_entry) + self.film_scale_entry = FCEntry() + self.film_scale_label = QtWidgets.QLabel("Scale Stroke:") + self.film_scale_label.setToolTip( + "Scale the line stroke thickness of each feature in the SVG file.\n" + "It means that the line that envelope each SVG feature will be thicker or thinner,\n" + "therefore the fine features may be more affected by this parameter." + ) + tf_form_layout.addRow(self.film_scale_label, self.film_scale_entry) + # Buttons hlay = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay) @@ -136,10 +146,6 @@ class Film(FlatCAMTool): self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed) - ## Initialize form - self.film_type.set_value('neg') - self.boundary_entry.set_value(0.0) - def on_type_obj_index_changed(self, index): obj_type = self.tf_type_obj_combo.currentIndex() self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) @@ -151,25 +157,58 @@ class Film(FlatCAMTool): self.tf_box_combo.setCurrentIndex(0) def run(self): + self.app.report_usage("ToolFilm()") + FlatCAMTool.run(self) + self.set_tool_ui() self.app.ui.notebook.setTabText(2, "Film Tool") def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+L', **kwargs) + def set_tool_ui(self): + self.reset_fields() + + f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg' + self.film_type.set_value(str(f_type)) + + b_entry = self.app.defaults[ "tools_film_boundary"] if self.app.defaults[ "tools_film_boundary"] else 0.0 + self.boundary_entry.set_value(float(b_entry)) + + scale_stroke_width = self.app.defaults["tools_film_scale"] if self.app.defaults["tools_film_scale"] else 0.0 + self.film_scale_entry.set_value(int(scale_stroke_width)) + def on_film_creation(self): try: name = self.tf_object_combo.currentText() except: - self.app.inform.emit("[error_notcl] No Film object selected. Load a Film object and retry.") + self.app.inform.emit("[ERROR_NOTCL] No FlatCAM object selected. Load an object for Film and retry.") return + try: boxname = self.tf_box_combo.currentText() except: - self.app.inform.emit("[error_notcl] No Box object selected. Load a Box object and retry.") + self.app.inform.emit("[ERROR_NOTCL] No FlatCAM object selected. Load an object for Box and retry.") + return + + try: + border = float(self.boundary_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + border = float(self.boundary_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + scale_stroke_width = int(self.film_scale_entry.get_value()) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") return - border = float(self.boundary_entry.get_value()) if border is None: border = 0 @@ -177,32 +216,36 @@ class Film(FlatCAMTool): if self.film_type.get_value() == "pos": try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive", - directory=self.app.get_last_save_folder(), filter="*.svg") + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export SVG positive", + directory=self.app.get_last_save_folder() + '/' + name, + filter="*.svg") except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive") filename = str(filename) if str(filename) == "": - self.app.inform.emit("Export SVG positive cancelled.") + self.app.inform.emit("[WARNING_NOTCL]Export SVG positive cancelled.") return else: - self.app.export_svg_black(name, boxname, filename) + self.app.export_svg_black(name, boxname, filename, scale_factor=scale_stroke_width) else: try: - filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative", - directory=self.app.get_last_save_folder(), filter="*.svg") + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export SVG negative", + directory=self.app.get_last_save_folder() + '/' + name, + filter="*.svg") except TypeError: filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative") filename = str(filename) if str(filename) == "": - self.app.inform.emit("Export SVG negative cancelled.") + self.app.inform.emit("[WARNING_NOTCL]Export SVG negative cancelled.") return else: - self.app.export_svg_negative(name, boxname, filename, border) + self.app.export_svg_negative(name, boxname, filename, border, scale_factor=scale_stroke_width) def reset_fields(self): self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) diff --git a/flatcamTools/ToolImage.py b/flatcamTools/ToolImage.py index 39761080..4e4d92c8 100644 --- a/flatcamTools/ToolImage.py +++ b/flatcamTools/ToolImage.py @@ -124,6 +124,17 @@ class ToolImage(FlatCAMTool): ## Signals self.import_button.clicked.connect(self.on_file_importimage) + def run(self): + self.app.report_usage("ToolImage()") + + FlatCAMTool.run(self) + self.set_tool_ui() + self.app.ui.notebook.setTabText(2, "Image Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, **kwargs) + + def set_tool_ui(self): ## Initialize form self.dpi_entry.set_value(96) self.image_type.set_value('black') @@ -132,10 +143,6 @@ class ToolImage(FlatCAMTool): self.mask_g_entry.set_value(250) self.mask_b_entry.set_value(250) - def run(self): - FlatCAMTool.run(self) - self.app.ui.notebook.setTabText(2, "Image Tool") - def on_file_importimage(self): """ Callback for menu item File->Import IMAGE. diff --git a/flatcamTools/ToolMeasurement.py b/flatcamTools/ToolMeasurement.py index b786ad88..683bbbb0 100644 --- a/flatcamTools/ToolMeasurement.py +++ b/flatcamTools/ToolMeasurement.py @@ -152,11 +152,19 @@ class Measurement(FlatCAMTool): self.measure_btn.clicked.connect(self.toggle) def run(self): + self.app.report_usage("ToolMeasurement()") + if self.app.tool_tab_locked is True: return - self.toggle() + self.set_tool_ui() + self.app.ui.notebook.setTabText(2, "Meas. Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs) + + def set_tool_ui(self): # Remove anything else in the GUI self.app.ui.tool_scroll_area.takeWidget() @@ -167,21 +175,6 @@ class Measurement(FlatCAMTool): self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab) self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower() self.show() - self.app.ui.notebook.setTabText(2, "Meas. Tool") - - def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs) - - def on_key_release_meas(self, event): - if event.key == 'escape': - # abort the measurement action - self.toggle() - return - - if event.key == 'G': - # toggle grid status - self.app.ui.grid_snap_btn.trigger() - return def toggle(self): # the self.active var is doing the 'toggle' @@ -264,6 +257,17 @@ class Measurement(FlatCAMTool): self.app.inform.emit("MEASURING: Click on the Start point ...") + def on_key_release_meas(self, event): + if event.key == 'escape': + # abort the measurement action + self.toggle() + return + + if event.key == 'G': + # toggle grid status + self.app.ui.grid_snap_btn.trigger() + return + def on_click_meas(self, event): # mouse click will be accepted only if the left button is clicked # this is necessary because right mouse click and middle mouse click diff --git a/flatcamTools/ToolMove.py b/flatcamTools/ToolMove.py index b7fd1e2e..971d901f 100644 --- a/flatcamTools/ToolMove.py +++ b/flatcamTools/ToolMove.py @@ -34,10 +34,44 @@ class ToolMove(FlatCAMTool): FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs) def run(self): + self.app.report_usage("ToolMove()") + if self.app.tool_tab_locked is True: return self.toggle() + def toggle(self): + if self.isVisible(): + self.setVisible(False) + + self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move) + self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click) + self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press) + self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot) + + self.clicked_move = 0 + + # signal that there is no command active + self.app.command_active = None + + # delete the selection box + self.delete_shape() + return + else: + self.setVisible(True) + # signal that there is a command active and it is 'Move' + self.app.command_active = "Move" + + if self.app.collection.get_selected(): + self.app.inform.emit("MOVE: Click on the Start point ...") + # draw the selection box + self.draw_sel_bbox() + else: + self.setVisible(False) + # signal that there is no command active + self.app.command_active = None + self.app.inform.emit("[WARNING_NOTCL]MOVE action cancelled. No object(s) to move.") + def on_left_click(self, event): # mouse click will be accepted only if the left button is clicked # this is necessary because right mouse click and middle mouse click @@ -83,7 +117,7 @@ class ToolMove(FlatCAMTool): try: if not obj_list: - self.app.inform.emit("[warning_notcl] No object(s) selected.") + self.app.inform.emit("[WARNING_NOTCL] No object(s) selected.") return "fail" else: for sel_obj in obj_list: @@ -99,7 +133,7 @@ class ToolMove(FlatCAMTool): # self.app.collection.set_active(sel_obj.options['name']) except Exception as e: proc.done() - self.app.inform.emit('[error_notcl] ' + self.app.inform.emit('[ERROR_NOTCL] ' 'ToolMove.on_left_click() --> %s' % str(e)) return "fail" proc.done() @@ -114,7 +148,7 @@ class ToolMove(FlatCAMTool): return except TypeError: - self.app.inform.emit('[error_notcl] ' + self.app.inform.emit('[ERROR_NOTCL] ' 'ToolMove.on_left_click() --> Error when mouse left click.') return @@ -142,42 +176,10 @@ class ToolMove(FlatCAMTool): def on_key_press(self, event): if event.key == 'escape': # abort the move action - self.app.inform.emit("[warning_notcl]Move action cancelled.") + self.app.inform.emit("[WARNING_NOTCL]Move action cancelled.") self.toggle() return - def toggle(self): - if self.isVisible(): - self.setVisible(False) - - self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move) - self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click) - self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press) - self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot) - - self.clicked_move = 0 - - # signal that there is no command active - self.app.command_active = None - - # delete the selection box - self.delete_shape() - return - else: - self.setVisible(True) - # signal that there is a command active and it is 'Move' - self.app.command_active = "Move" - - if self.app.collection.get_selected(): - self.app.inform.emit("MOVE: Click on the Start point ...") - # draw the selection box - self.draw_sel_bbox() - else: - self.setVisible(False) - # signal that there is no command active - self.app.command_active = None - self.app.inform.emit("[warning_notcl]MOVE action cancelled. No object(s) to move.") - def draw_sel_bbox(self): xminlist = [] yminlist = [] @@ -186,7 +188,7 @@ class ToolMove(FlatCAMTool): obj_list = self.app.collection.get_selected() if not obj_list: - self.app.inform.emit("[warning_notcl]Object(s) not selected") + self.app.inform.emit("[WARNING_NOTCL]Object(s) not selected") self.toggle() else: # if we have an object selected then we can safely activate the mouse events diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index 7cd4bd70..3768bd65 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -35,6 +35,7 @@ class NonCopperClear(FlatCAMTool, Gerber): self.object_combo.setModel(self.app.collection) self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.object_combo.setCurrentIndex(1) + self.object_label = QtWidgets.QLabel("Gerber:") self.object_label.setToolTip( "Gerber object to be cleared of excess copper. " @@ -97,7 +98,7 @@ class NonCopperClear(FlatCAMTool, Gerber): self.addtool_entry_lbl.setToolTip( "Diameter for the new tool to add in the Tool Table" ) - self.addtool_entry = FloatEntry() + self.addtool_entry = FCEntry() # hlay.addWidget(self.addtool_label) # hlay.addStretch() @@ -151,7 +152,7 @@ class NonCopperClear(FlatCAMTool, Gerber): "due of too many paths." ) grid3.addWidget(nccoverlabel, 1, 0) - self.ncc_overlap_entry = FloatEntry() + self.ncc_overlap_entry = FCEntry() grid3.addWidget(self.ncc_overlap_entry, 1, 1) nccmarginlabel = QtWidgets.QLabel('Margin:') @@ -159,7 +160,7 @@ class NonCopperClear(FlatCAMTool, Gerber): "Bounding box margin." ) grid3.addWidget(nccmarginlabel, 2, 0) - self.ncc_margin_entry = FloatEntry() + self.ncc_margin_entry = FCEntry() grid3.addWidget(self.ncc_margin_entry, 2, 1) # Method @@ -237,13 +238,16 @@ class NonCopperClear(FlatCAMTool, Gerber): FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs) def run(self): + self.app.report_usage("ToolNonCopperClear()") + FlatCAMTool.run(self) - self.tools_frame.show() - self.set_ui() + self.set_tool_ui() self.build_ui() self.app.ui.notebook.setTabText(2, "NCC Tool") - def set_ui(self): + def set_tool_ui(self): + self.tools_frame.show() + self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"]) self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"]) self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"]) @@ -408,7 +412,6 @@ class NonCopperClear(FlatCAMTool, Gerber): self.tools_table.setMinimumHeight(self.tools_table.getHeight()) self.tools_table.setMaximumHeight(self.tools_table.getHeight()) - self.app.report_usage("gerber_on_ncc_button") self.ui_connect() def ui_connect(self): @@ -428,10 +431,19 @@ class NonCopperClear(FlatCAMTool, Gerber): if dia: tool_dia = dia else: - tool_dia = self.addtool_entry.get_value() + try: + tool_dia = float(self.addtool_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tool_dia = float(self.addtool_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return if tool_dia is None: self.build_ui() - self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.") + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.") return # construct a list of all 'tooluid' in the self.tools @@ -455,7 +467,7 @@ class NonCopperClear(FlatCAMTool, Gerber): if float('%.4f' % tool_dia) in tool_dias: if muted is None: - self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.") + self.app.inform.emit("[WARNING_NOTCL]Adding tool cancelled. Tool already in Tool Table.") self.tools_table.itemChanged.connect(self.on_tool_edit) return else: @@ -485,7 +497,18 @@ class NonCopperClear(FlatCAMTool, Gerber): tool_dias.append(float('%.4f' % v[tool_v])) for row in range(self.tools_table.rowCount()): - new_tool_dia = float(self.tools_table.item(row, 1).text()) + + try: + new_tool_dia = float(self.tools_table.item(row, 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + tooluid = int(self.tools_table.item(row, 3).text()) # identify the tool that was edited and get it's tooluid @@ -502,7 +525,7 @@ class NonCopperClear(FlatCAMTool, Gerber): break restore_dia_item = self.tools_table.item(row, 1) restore_dia_item.setText(str(old_tool_dia)) - self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.") + self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.") self.build_ui() def on_tool_delete(self, rows_to_delete=None, all=None): @@ -541,7 +564,7 @@ class NonCopperClear(FlatCAMTool, Gerber): self.ncc_tools.pop(t, None) except AttributeError: - self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.") + self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a tool to delete.") return except Exception as e: log.debug(str(e)) @@ -551,10 +574,28 @@ class NonCopperClear(FlatCAMTool, Gerber): def on_ncc(self): - over = self.ncc_overlap_entry.get_value() + try: + over = float(self.ncc_overlap_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + over = float(self.ncc_overlap_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return over = over if over else self.app.defaults["tools_nccoverlap"] - margin = self.ncc_margin_entry.get_value() + try: + margin = float(self.ncc_margin_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.ncc_margin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return margin = margin if margin else self.app.defaults["tools_nccmargin"] connect = self.ncc_connect_cb.get_value() @@ -574,7 +615,7 @@ class NonCopperClear(FlatCAMTool, Gerber): try: self.ncc_obj = self.app.collection.get_by_name(self.obj_name) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % self.obj_name) return "Could not retrieve object: %s" % self.obj_name @@ -582,7 +623,7 @@ class NonCopperClear(FlatCAMTool, Gerber): try: bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre) except AttributeError: - self.app.inform.emit("[error_notcl]No Gerber file available.") + self.app.inform.emit("[ERROR_NOTCL]No Gerber file available.") return # calculate the empty area by substracting the solid_geometry from the object bounding box geometry @@ -707,14 +748,14 @@ class NonCopperClear(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, initialize) except Exception as e: proc.done() - self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper() --> %s' % str(e)) + self.app.inform.emit('[ERROR_NOTCL] NCCTool.clear_non_copper() --> %s' % str(e)) return proc.done() if app_obj.poly_not_cleared is False: self.app.inform.emit('[success] NCC Tool finished.') else: - self.app.inform.emit('[warning_notcl] NCC Tool finished but some PCB features could not be cleared. ' + self.app.inform.emit('[WARNING_NOTCL] NCC Tool finished but some PCB features could not be cleared. ' 'Check the result.') # reset the variable for next use app_obj.poly_not_cleared = False @@ -858,7 +899,7 @@ class NonCopperClear(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, initialize_rm) except Exception as e: proc.done() - self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper_rest() --> %s' % str(e)) + self.app.inform.emit('[ERROR_NOTCL] NCCTool.clear_non_copper_rest() --> %s' % str(e)) return if app_obj.poly_not_cleared is True: @@ -866,7 +907,7 @@ class NonCopperClear(FlatCAMTool, Gerber): # focus on Selected Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab) else: - self.app.inform.emit('[error_notcl] NCC Tool finished but could not clear the object ' + self.app.inform.emit('[ERROR_NOTCL] NCC Tool finished but could not clear the object ' 'with current settings.') # focus on Project Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) @@ -882,3 +923,7 @@ class NonCopperClear(FlatCAMTool, Gerber): # Background self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + + def reset_fields(self): + self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 1bf3551a..0604d9ac 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -33,6 +33,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.object_combo.setModel(self.app.collection) self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) self.object_combo.setCurrentIndex(1) + self.object_label = QtWidgets.QLabel("Geometry:") self.object_label.setToolTip( "Geometry object to be painted. " @@ -94,7 +95,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.addtool_entry_lbl.setToolTip( "Diameter for the new tool." ) - self.addtool_entry = FloatEntry() + self.addtool_entry = FCEntry() # hlay.addWidget(self.addtool_label) # hlay.addStretch() @@ -146,7 +147,7 @@ class ToolPaint(FlatCAMTool, Gerber): "due of too many paths." ) grid3.addWidget(ovlabel, 1, 0) - self.paintoverlap_entry = LengthEntry() + self.paintoverlap_entry = FCEntry() grid3.addWidget(self.paintoverlap_entry, 1, 1) # Margin @@ -157,7 +158,7 @@ class ToolPaint(FlatCAMTool, Gerber): "be painted." ) grid3.addWidget(marginlabel, 2, 0) - self.paintmargin_entry = LengthEntry() + self.paintmargin_entry = FCEntry() grid3.addWidget(self.paintmargin_entry, 2, 1) # Method @@ -294,9 +295,10 @@ class ToolPaint(FlatCAMTool, Gerber): FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **kwargs) def run(self): + self.app.report_usage("ToolPaint()") + FlatCAMTool.run(self) - self.tools_frame.show() - self.set_ui() + self.set_tool_ui() self.app.ui.notebook.setTabText(2, "Paint Tool") def on_radio_selection(self): @@ -320,7 +322,10 @@ class ToolPaint(FlatCAMTool, Gerber): self.deltool_btn.setDisabled(False) self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu) - def set_ui(self): + def set_tool_ui(self): + self.tools_frame.show() + self.reset_fields() + ## Init the GUI interface self.paintmargin_entry.set_value(self.default_data["paintmargin"]) self.paintmethod_combo.set_value(self.default_data["paintmethod"]) @@ -484,10 +489,20 @@ class ToolPaint(FlatCAMTool, Gerber): if dia: tool_dia = dia else: - tool_dia = self.addtool_entry.get_value() + try: + tool_dia = float(self.addtool_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tool_dia = float(self.addtool_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + if tool_dia is None: self.build_ui() - self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.") + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.") return # construct a list of all 'tooluid' in the self.tools @@ -511,7 +526,7 @@ class ToolPaint(FlatCAMTool, Gerber): if float('%.4f' % tool_dia) in tool_dias: if muted is None: - self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.") + self.app.inform.emit("[WARNING_NOTCL]Adding tool cancelled. Tool already in Tool Table.") self.tools_table.itemChanged.connect(self.on_tool_edit) return else: @@ -544,7 +559,16 @@ class ToolPaint(FlatCAMTool, Gerber): tool_dias.append(float('%.4f' % v[tool_v])) for row in range(self.tools_table.rowCount()): - new_tool_dia = float(self.tools_table.item(row, 1).text()) + try: + new_tool_dia = float(self.tools_table.item(row, 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return tooluid = int(self.tools_table.item(row, 3).text()) # identify the tool that was edited and get it's tooluid @@ -561,7 +585,7 @@ class ToolPaint(FlatCAMTool, Gerber): break restore_dia_item = self.tools_table.item(row, 1) restore_dia_item.setText(str(old_tool_dia)) - self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.") + self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.") self.build_ui() # def on_tool_copy(self, all=None): @@ -594,7 +618,7 @@ class ToolPaint(FlatCAMTool, Gerber): # print("COPIED", self.paint_tools[td]) # self.build_ui() # except AttributeError: - # self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.") + # self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.") # self.build_ui() # return # except Exception as e: @@ -602,7 +626,7 @@ class ToolPaint(FlatCAMTool, Gerber): # # deselect the table # # self.ui.geo_tools_table.clearSelection() # else: - # self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.") + # self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.") # self.build_ui() # return # else: @@ -658,7 +682,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.paint_tools.pop(t, None) except AttributeError: - self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.") + self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a tool to delete.") return except Exception as e: log.debug(str(e)) @@ -669,9 +693,18 @@ class ToolPaint(FlatCAMTool, Gerber): def on_paint_button_click(self): self.app.report_usage("geometry_on_paint_button") - self.app.inform.emit("[warning_notcl]Click inside the desired polygon.") + self.app.inform.emit("[WARNING_NOTCL]Click inside the desired polygon.") + try: + overlap = float(self.paintoverlap_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + overlap = float(self.paintoverlap_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return - overlap = self.paintoverlap_entry.get_value() connect = self.pathconnect_cb.get_value() contour = self.paintcontour_cb.get_value() select_method = self.selectmethod_combo.get_value() @@ -682,11 +715,11 @@ class ToolPaint(FlatCAMTool, Gerber): try: self.paint_obj = self.app.collection.get_by_name(str(self.obj_name)) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % self.obj_name) return if self.paint_obj is None: - self.app.inform.emit("[error_notcl]Object not found: %s" % self.paint_obj) + self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % self.paint_obj) return o_name = '%s_multitool_paint' % (self.obj_name) @@ -699,7 +732,7 @@ class ToolPaint(FlatCAMTool, Gerber): contour=contour) if select_method == "single": - self.app.inform.emit("[warning_notcl]Click inside the desired polygon.") + self.app.inform.emit("[WARNING_NOTCL]Click inside the desired polygon.") # use the first tool in the tool table; get the diameter tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text())) @@ -742,12 +775,22 @@ class ToolPaint(FlatCAMTool, Gerber): # poly = find_polygon(self.solid_geometry, inside_pt) poly = obj.find_polygon(inside_pt) paint_method = self.paintmethod_combo.get_value() - paint_margin = self.paintmargin_entry.get_value() + + try: + paint_margin = float(self.paintmargin_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return # No polygon? if poly is None: self.app.log.warning('No polygon found.') - self.app.inform.emit('[warning] No polygon found.') + self.app.inform.emit('[WARNING] No polygon found.') return proc = self.app.proc_container.new("Painting polygon.") @@ -792,7 +835,7 @@ class ToolPaint(FlatCAMTool, Gerber): geo_obj.solid_geometry += list(cp.get_objects()) return cp else: - self.app.inform.emit('[error_notcl] Geometry could not be painted completely') + self.app.inform.emit('[ERROR_NOTCL] Geometry could not be painted completely') return None geo_obj.solid_geometry = [] @@ -807,7 +850,7 @@ class ToolPaint(FlatCAMTool, Gerber): except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit( - "[error] Could not do Paint. Try a different combination of parameters. " + "[ERROR] Could not do Paint. Try a different combination of parameters. " "Or a different strategy of paint\n%s" % str(e)) return @@ -838,7 +881,7 @@ class ToolPaint(FlatCAMTool, Gerber): # self.app.inform.emit("[success] Paint single polygon Done") # else: # print("[WARNING] Paint single polygon done with errors") - # self.app.inform.emit("[warning] Paint single polygon done with errors. " + # self.app.inform.emit("[WARNING] Paint single polygon done with errors. " # "%d area(s) could not be painted.\n" # "Use different paint parameters or edit the paint geometry and correct" # "the issue." @@ -849,7 +892,7 @@ class ToolPaint(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, gen_paintarea) except Exception as e: proc.done() - self.app.inform.emit('[error_notcl] PaintTool.paint_poly() --> %s' % str(e)) + self.app.inform.emit('[ERROR_NOTCL] PaintTool.paint_poly() --> %s' % str(e)) return proc.done() # focus on Selected Tab @@ -876,10 +919,19 @@ class ToolPaint(FlatCAMTool, Gerber): :return: """ paint_method = self.paintmethod_combo.get_value() - paint_margin = self.paintmargin_entry.get_value() + + try: + paint_margin = float(self.paintmargin_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return proc = self.app.proc_container.new("Painting polygon.") - name = outname if outname else self.obj_name + "_paint" over = overlap conn = connect @@ -984,7 +1036,7 @@ class ToolPaint(FlatCAMTool, Gerber): except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit( - "[error] Could not do Paint All. Try a different combination of parameters. " + "[ERROR] Could not do Paint All. Try a different combination of parameters. " "Or a different Method of paint\n%s" % str(e)) return @@ -1008,7 +1060,7 @@ class ToolPaint(FlatCAMTool, Gerber): if geo_obj.tools[tooluid]['solid_geometry']: has_solid_geo += 1 if has_solid_geo == 0: - self.app.inform.emit("[error] There is no Painting Geometry in the file.\n" + self.app.inform.emit("[ERROR] There is no Painting Geometry in the file.\n" "Usually it means that the tool diameter is too big for the painted geometry.\n" "Change the painting parameters and try again.") return @@ -1063,7 +1115,7 @@ class ToolPaint(FlatCAMTool, Gerber): except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit( - "[error] Could not do Paint All. Try a different combination of parameters. " + "[ERROR] Could not do Paint All. Try a different combination of parameters. " "Or a different Method of paint\n%s" % str(e)) return @@ -1093,7 +1145,7 @@ class ToolPaint(FlatCAMTool, Gerber): if geo_obj.tools[tooluid]['solid_geometry']: has_solid_geo += 1 if has_solid_geo == 0: - self.app.inform.emit("[error_notcl] There is no Painting Geometry in the file.\n" + self.app.inform.emit("[ERROR_NOTCL] There is no Painting Geometry in the file.\n" "Usually it means that the tool diameter is too big for the painted geometry.\n" "Change the painting parameters and try again.") return @@ -1125,3 +1177,6 @@ class ToolPaint(FlatCAMTool, Gerber): # Background self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + + def reset_fields(self): + self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index cdad3943..11549d08 100644 --- a/flatcamTools/ToolPanelize.py +++ b/flatcamTools/ToolPanelize.py @@ -44,6 +44,7 @@ class Panelize(FlatCAMTool): self.object_combo.setModel(self.app.collection) self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.object_combo.setCurrentIndex(1) + self.object_label = QtWidgets.QLabel("Object:") self.object_label.setToolTip( "Object to be panelized. This means that it will\n" @@ -76,6 +77,7 @@ class Panelize(FlatCAMTool): self.box_combo.setModel(self.app.collection) self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.box_combo.setCurrentIndex(1) + self.box_combo_label = QtWidgets.QLabel("Box Object:") self.box_combo_label.setToolTip( "The actual object that is used a container for the\n " @@ -84,8 +86,7 @@ class Panelize(FlatCAMTool): form_layout.addRow(self.box_combo_label, self.box_combo) ## Spacing Columns - self.spacing_columns = FloatEntry() - self.spacing_columns.set_value(0.0) + self.spacing_columns = FCEntry() self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:") self.spacing_columns_label.setToolTip( "Spacing between columns of the desired panel.\n" @@ -94,8 +95,7 @@ class Panelize(FlatCAMTool): form_layout.addRow(self.spacing_columns_label, self.spacing_columns) ## Spacing Rows - self.spacing_rows = FloatEntry() - self.spacing_rows.set_value(0.0) + self.spacing_rows = FCEntry() self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:") self.spacing_rows_label.setToolTip( "Spacing between rows of the desired panel.\n" @@ -104,8 +104,7 @@ class Panelize(FlatCAMTool): form_layout.addRow(self.spacing_rows_label, self.spacing_rows) ## Columns - self.columns = IntEntry() - self.columns.set_value(1) + self.columns = FCEntry() self.columns_label = QtWidgets.QLabel("Columns:") self.columns_label.setToolTip( "Number of columns of the desired panel" @@ -113,8 +112,7 @@ class Panelize(FlatCAMTool): form_layout.addRow(self.columns_label, self.columns) ## Rows - self.rows = IntEntry() - self.rows.set_value(1) + self.rows = FCEntry() self.rows_label = QtWidgets.QLabel("Rows:") self.rows_label.setToolTip( "Number of rows of the desired panel" @@ -132,8 +130,7 @@ class Panelize(FlatCAMTool): ) form_layout.addRow(self.constrain_cb) - self.x_width_entry = FloatEntry() - self.x_width_entry.set_value(0.0) + self.x_width_entry = FCEntry() self.x_width_lbl = QtWidgets.QLabel("Width (DX):") self.x_width_lbl.setToolTip( "The width (DX) within which the panel must fit.\n" @@ -141,8 +138,7 @@ class Panelize(FlatCAMTool): ) form_layout.addRow(self.x_width_lbl, self.x_width_entry) - self.y_height_entry = FloatEntry() - self.y_height_entry.set_value(0.0) + self.y_height_entry = FCEntry() self.y_height_lbl = QtWidgets.QLabel("Height (DY):") self.y_height_lbl.setToolTip( "The height (DY)within which the panel must fit.\n" @@ -183,6 +179,47 @@ class Panelize(FlatCAMTool): # flag to signal the constrain was activated self.constrain_flag = False + def run(self): + self.app.report_usage("ToolPanelize()") + + FlatCAMTool.run(self) + self.set_tool_ui() + self.app.ui.notebook.setTabText(2, "Panel. Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs) + + def set_tool_ui(self): + self.reset_fields() + + sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \ + self.app.defaults["tools_panelize_spacing_columns"] else 0.0 + self.spacing_columns.set_value(float(sp_c)) + + sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \ + self.app.defaults["tools_panelize_spacing_rows"] else 0.0 + self.spacing_rows.set_value(float(sp_r)) + + rr = self.app.defaults["tools_panelize_rows"] if \ + self.app.defaults["tools_panelize_rows"] else 0.0 + self.rows.set_value(int(rr)) + + cc = self.app.defaults["tools_panelize_columns"] if \ + self.app.defaults["tools_panelize_columns"] else 0.0 + self.columns.set_value(int(cc)) + + c_cb = self.app.defaults["tools_panelize_constrain"] if \ + self.app.defaults["tools_panelize_constrain"] else False + self.constrain_cb.set_value(c_cb) + + x_w = self.app.defaults["tools_panelize_constrainx"] if \ + self.app.defaults["tools_panelize_constrainx"] else 0.0 + self.x_width_entry.set_value(float(x_w)) + + y_w = self.app.defaults["tools_panelize_constrainy"] if \ + self.app.defaults["tools_panelize_constrainy"] else 0.0 + self.y_height_entry.set_value(float(y_w)) + def on_type_obj_index_changed(self): obj_type = self.type_obj_combo.currentIndex() self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) @@ -193,13 +230,6 @@ class Panelize(FlatCAMTool): self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.box_combo.setCurrentIndex(0) - def run(self): - FlatCAMTool.run(self) - self.app.ui.notebook.setTabText(2, "Panel. Tool") - - def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs) - def on_panelize(self): name = self.object_combo.currentText() @@ -207,13 +237,13 @@ class Panelize(FlatCAMTool): try: obj = self.app.collection.get_by_name(str(name)) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name) return "Could not retrieve object: %s" % name panel_obj = obj if panel_obj is None: - self.app.inform.emit("[error_notcl]Object not found: %s" % panel_obj) + self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % panel_obj) return "Object not found: %s" % panel_obj boxname = self.box_combo.currentText() @@ -221,32 +251,89 @@ class Panelize(FlatCAMTool): try: box = self.app.collection.get_by_name(boxname) except: - self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % boxname) + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % boxname) return "Could not retrieve object: %s" % boxname if box is None: - self.app.inform.emit("[warning]No object Box. Using instead %s" % panel_obj) + self.app.inform.emit("[WARNING]No object Box. Using instead %s" % panel_obj) box = panel_obj self.outname = name + '_panelized' - spacing_columns = self.spacing_columns.get_value() + try: + spacing_columns = float(self.spacing_columns.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + spacing_columns = float(self.spacing_columns.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return spacing_columns = spacing_columns if spacing_columns is not None else 0 - spacing_rows = self.spacing_rows.get_value() + try: + spacing_rows = float(self.spacing_rows.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + spacing_rows = float(self.spacing_rows.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return spacing_rows = spacing_rows if spacing_rows is not None else 0 - rows = self.rows.get_value() + try: + rows = int(self.rows.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + rows = float(self.rows.get_value().replace(',', '.')) + rows = int(rows) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return rows = rows if rows is not None else 1 - columns = self.columns.get_value() + try: + columns = int(self.columns.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + columns = float(self.columns.get_value().replace(',', '.')) + columns = int(columns) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return columns = columns if columns is not None else 1 - constrain_dx = self.x_width_entry.get_value() - constrain_dy = self.y_height_entry.get_value() + try: + constrain_dx = float(self.x_width_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + constrain_dx = float(self.x_width_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + try: + constrain_dy = float(self.y_height_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + constrain_dy = float(self.y_height_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return if 0 in {columns, rows}: - self.app.inform.emit("[error_notcl]Columns or Rows are zero value. Change them to a positive integer.") + self.app.inform.emit("[ERROR_NOTCL]Columns or Rows are zero value. Change them to a positive integer.") return "Columns or Rows are zero value. Change them to a positive integer." xmin, ymin, xmax, ymax = box.bounds() @@ -342,7 +429,7 @@ class Panelize(FlatCAMTool): # self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True) # # else: - # self.app.inform.emit("[error_notcl] Obj is None") + # self.app.inform.emit("[ERROR_NOTCL] Obj is None") # return "ERROR: Obj is None" # panelize() @@ -455,7 +542,7 @@ class Panelize(FlatCAMTool): self.app.inform.emit("[success]Panel done...") else: self.constrain_flag = False - self.app.inform.emit("[warning] Too big for the constrain area. Final panel has %s columns and %s rows" % + self.app.inform.emit("[WARNING] Too big for the constrain area. Final panel has %s columns and %s rows" % (columns, rows)) proc = self.app.proc_container.new("Generating panel ... Please wait.") diff --git a/flatcamTools/ToolProperties.py b/flatcamTools/ToolProperties.py index 06222f8a..1cfb8fd7 100644 --- a/flatcamTools/ToolProperties.py +++ b/flatcamTools/ToolProperties.py @@ -42,24 +42,26 @@ class Properties(FlatCAMTool): self.vlay.setStretch(0,0) def run(self): + self.app.report_usage("ToolProperties()") if self.app.tool_tab_locked is True: return - - # this reset the TreeWidget - self.treeWidget.clear() - self.properties_frame.show() - + self.set_tool_ui() FlatCAMTool.run(self) self.properties() def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='P', **kwargs) + def set_tool_ui(self): + # this reset the TreeWidget + self.treeWidget.clear() + self.properties_frame.show() + def properties(self): obj_list = self.app.collection.get_selected() if not obj_list: - self.app.inform.emit("[error_notcl] Properties Tool was not displayed. No object selected.") + self.app.inform.emit("[ERROR_NOTCL] Properties Tool was not displayed. No object selected.") self.app.ui.notebook.setTabText(2, "Tools") self.properties_frame.hide() self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) diff --git a/flatcamTools/ToolShell.py b/flatcamTools/ToolShell.py index d24b56b8..80e94437 100644 --- a/flatcamTools/ToolShell.py +++ b/flatcamTools/ToolShell.py @@ -234,7 +234,7 @@ class TermWidget(QWidget): """ Convert text to HTML for inserting it to browser """ - assert style in ('in', 'out', 'err') + assert style in ('in', 'out', 'err', 'warning', 'success') text = html.escape(text) text = text.replace('\n', '
') @@ -243,6 +243,10 @@ class TermWidget(QWidget): text = '%s' % text elif style == 'err': text = '%s' % text + elif style == 'warning': + text = '%s' % text + elif style == 'success': + text = '%s' % text else: text = '%s' % text # without span
is ignored!!! @@ -304,6 +308,16 @@ class TermWidget(QWidget): """ self._append_to_browser('out', text) + def append_success(self, text): + """Appent text to output widget + """ + self._append_to_browser('success', text) + + def append_warning(self, text): + """Appent text to output widget + """ + self._append_to_browser('warning', text) + def append_error(self, text): """Appent error text to output widget. Text is drawn with red background """ diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 7d641494..3652e2e6 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -355,7 +355,17 @@ class ToolTransform(FlatCAMTool): self.offx_entry.returnPressed.connect(self.on_offx) self.offy_entry.returnPressed.connect(self.on_offy) + def run(self): + self.app.report_usage("ToolTransform()") + FlatCAMTool.run(self) + self.set_tool_ui() + self.app.ui.notebook.setTabText(2, "Transform Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs) + + def set_tool_ui(self): ## Initialize form self.rotate_entry.set_value('0') self.skewx_entry.set_value('0') @@ -366,19 +376,17 @@ class ToolTransform(FlatCAMTool): self.offy_entry.set_value('0') self.flip_ref_cb.setChecked(False) - def run(self): - FlatCAMTool.run(self) - self.app.ui.notebook.setTabText(2, "Transform Tool") - - def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs) - def on_rotate(self): try: value = float(self.rotate_entry.get_value()) - except Exception as e: - self.app.inform.emit("[error] Failed to rotate due of: %s" % str(e)) - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.rotate_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Rotate, " + "use a number.") + return self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value]}) # self.on_rotate_action(value) @@ -405,9 +413,15 @@ class ToolTransform(FlatCAMTool): def on_skewx(self): try: value = float(self.skewx_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Skew!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew X, " + "use a number.") + return + # self.on_skew("X", value) axis = 'X' self.app.worker_task.emit({'fcn': self.on_skew, @@ -417,9 +431,15 @@ class ToolTransform(FlatCAMTool): def on_skewy(self): try: value = float(self.skewy_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Skew!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew Y, " + "use a number.") + return + # self.on_skew("Y", value) axis = 'Y' self.app.worker_task.emit({'fcn': self.on_skew, @@ -429,9 +449,15 @@ class ToolTransform(FlatCAMTool): def on_scalex(self): try: xvalue = float(self.scalex_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Scale!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + xvalue = float(self.scalex_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale X, " + "use a number.") + return + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing if xvalue == 0: xvalue = 1 @@ -457,9 +483,15 @@ class ToolTransform(FlatCAMTool): xvalue = 1 try: yvalue = float(self.scaley_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Scale!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + yvalue = float(self.scaley_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale Y, " + "use a number.") + return + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing if yvalue == 0: yvalue = 1 @@ -480,9 +512,15 @@ class ToolTransform(FlatCAMTool): def on_offx(self): try: value = float(self.offx_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Offset!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset X, " + "use a number.") + return + # self.on_offset("X", value) axis = 'X' self.app.worker_task.emit({'fcn': self.on_offset, @@ -492,9 +530,15 @@ class ToolTransform(FlatCAMTool): def on_offy(self): try: value = float(self.offy_entry.get_value()) - except: - self.app.inform.emit("[warning_notcl] No value for Offset!") - return + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset Y, " + "use a number.") + return + # self.on_offset("Y", value) axis = 'Y' self.app.worker_task.emit({'fcn': self.on_offset, @@ -509,7 +553,7 @@ class ToolTransform(FlatCAMTool): ymaxlist = [] if not obj_list: - self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to rotate!") + self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to rotate!") return else: with self.app.proc_container.new("Appying Rotate"): @@ -550,7 +594,7 @@ class ToolTransform(FlatCAMTool): self.app.progress.emit(100) except Exception as e: - self.app.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) return def on_flip(self, axis): @@ -561,7 +605,7 @@ class ToolTransform(FlatCAMTool): ymaxlist = [] if not obj_list: - self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to flip!") + self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to flip!") return else: with self.app.proc_container.new("Applying Flip"): @@ -623,7 +667,7 @@ class ToolTransform(FlatCAMTool): self.app.progress.emit(100) except Exception as e: - self.app.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return def on_skew(self, axis, num): @@ -632,7 +676,7 @@ class ToolTransform(FlatCAMTool): yminlist = [] if not obj_list: - self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to shear/skew!") + self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to shear/skew!") return else: with self.app.proc_container.new("Applying Skew"): @@ -670,7 +714,7 @@ class ToolTransform(FlatCAMTool): self.app.progress.emit(100) except Exception as e: - self.app.inform.emit("[error_notcl] Due of %s, Skew action was not executed." % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Skew action was not executed." % str(e)) return def on_scale(self, axis, xfactor, yfactor, point=None): @@ -681,7 +725,7 @@ class ToolTransform(FlatCAMTool): ymaxlist = [] if not obj_list: - self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to scale!") + self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to scale!") return else: with self.app.proc_container.new("Applying Scale"): @@ -725,7 +769,7 @@ class ToolTransform(FlatCAMTool): self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis)) self.app.progress.emit(100) except Exception as e: - self.app.inform.emit("[error_notcl] Due of %s, Scale action was not executed." % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e)) return def on_offset(self, axis, num): @@ -734,7 +778,7 @@ class ToolTransform(FlatCAMTool): yminlist = [] if not obj_list: - self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to offset!") + self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to offset!") return else: with self.app.proc_container.new("Applying Offset"): @@ -771,7 +815,7 @@ class ToolTransform(FlatCAMTool): self.app.progress.emit(100) except Exception as e: - self.app.inform.emit("[error_notcl] Due of %s, Offset action was not executed." % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Offset action was not executed." % str(e)) return # end of file \ No newline at end of file diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index d322132f..e5e1e84f 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -5,7 +5,8 @@ from flatcamTools.ToolPanelize import Panelize from flatcamTools.ToolFilm import Film from flatcamTools.ToolMove import ToolMove from flatcamTools.ToolDblSided import DblSidedTool -from flatcamTools.ToolCutout import ToolCutout + +from flatcamTools.ToolCutOut import ToolCutOut from flatcamTools.ToolCalculators import ToolCalculator from flatcamTools.ToolProperties import Properties from flatcamTools.ToolImage import ToolImage diff --git a/postprocessors/Toolchange_Probe_MACH3.py b/postprocessors/Toolchange_Probe_MACH3.py new file mode 100644 index 00000000..0ce20de1 --- /dev/null +++ b/postprocessors/Toolchange_Probe_MACH3.py @@ -0,0 +1,261 @@ +from FlatCAMPostProc import * + + +class Toolchange_Probe_MACH3(FlatCAMPostProc): + + coordinate_format = "%.*f" + feedrate_format = '%.*f' + + def start_code(self, p): + units = ' ' + str(p['units']).lower() + coords_xy = p['toolchange_xy'] + gcode = '' + + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + + if str(p['options']['type']) == 'Geometry': + gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + + gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' + + if str(p['options']['type']) == 'Geometry': + gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n' + + gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' + gcode += '(Feedrate Probe ' + str(p['feedrate_probe']) + units + '/min' + ')\n' + '\n' + gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n' + + if str(p['options']['type']) == 'Geometry': + if p['multidepth'] is True: + gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \ + str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n' + + gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' + gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' + + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' + + gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' + gcode += '(Z End: ' + str(p['endz']) + units + ')\n' + gcode += '(Z Probe Depth: ' + str(p['z_pdepth']) + units + ')\n' + gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' + + if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + else: + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' + + gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) + + gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') + gcode += 'G90\n' + gcode += 'G17\n' + gcode += 'G94\n' + + return gcode + + def startz_code(self, p): + return '' + + def lift_code(self, p): + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + + def down_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + + def toolchange_code(self, p): + toolchangez = p.toolchangez + toolchangexy = p.toolchange_xy + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + + no_drills = 1 + + if int(p.tool) == 1 and p.startz is not None: + toolchangez = p.startz + + if p.units.upper() == 'MM': + toolC_formatted = format(p.toolC, '.2f') + else: + toolC_formatted = format(p.toolC, '.4f') + + if str(p['options']['type']) == 'Excellon': + for i in p['options']['Tools_in_use']: + if i[0] == p.tool: + no_drills = i[2] + + if toolchangexy is not None: + gcode = """ +T{tool} +M5 +M6 +G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +(MSG, Change to Tool Dia = {toolC} ||| Drills for this tool = {t_drills} ||| Tool Probing MACH3) +M0 +G00 Z{z_move} +F{feedrate_probe} +G31 Z{z_pdepth} +G92 Z0 +G00 Z{z_move} +F{feedrate_probe_slow} +G31 Z{z_pdepth} +G92 Z0 +(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...) +M0 +G00 Z{z_move} +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move), + feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)), + feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))), + z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + else: + gcode = """ +T{tool} +M5 +M6 +G00 Z{toolchangez} +(MSG, Change to Tool Dia = {toolC} ||| Drills for this tool = {t_drills} ||| Tool Probing MACH3) +M0 +G00 Z{z_move} +F{feedrate_probe} +G31 Z{z_pdepth} +G92 Z0 +G00 Z{z_move} +F{feedrate_probe_slow} +G31 Z{z_pdepth} +G92 Z0 +(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...) +M0 +G00 Z{z_move} +""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move), + feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)), + feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))), + z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + # if f_plunge is True: + # gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + else: + if toolchangexy is not None: + gcode = """ +T{tool} +M5 +M6 +G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +(MSG, Change to Tool Dia = {toolC} ||| Tool Probing MACH3) +M0 +G00 Z{z_move} +F{feedrate_probe} +G31 Z{z_pdepth} +G92 Z0 +G00 Z{z_move} +F{feedrate_probe_slow} +G31 Z{z_pdepth} +G92 Z0 +(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...) +M0 +G00 Z{z_move} +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move), + feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)), + feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))), + z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """ +T{tool} +M5 +M6 +G00 Z{toolchangez} +(MSG, Change to Tool Dia = {toolC} ||| Tool Probing MACH3) +M0 +G00 Z{z_move} +F{feedrate_probe} +G31 Z{z_pdepth} +G92 Z0 +G00 Z{z_move} +F{feedrate_probe_slow} +G31 Z{z_pdepth} +G92 Z0 +(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...) +M0 +G00 Z{z_move} +""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move), + feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)), + feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))), + z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth), + tool=int(p.tool), + toolC=toolC_formatted) + + # if f_plunge is True: + # gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + def up_to_zero_code(self, p): + return 'G01 Z0' + + def position_code(self, p): + return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \ + (p.coords_decimals, p.x, p.coords_decimals, p.y) + + def rapid_code(self, p): + return ('G00 ' + self.position_code(p)).format(**p) + + def linear_code(self, p): + return ('G01 ' + self.position_code(p)).format(**p) + + def end_code(self, p): + coords_xy = p['toolchange_xy'] + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + return gcode + + def feedrate_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate)) + + def feedrate_z_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z)) + + def spindle_code(self, p): + if p.spindlespeed: + return 'M03 S' + str(p.spindlespeed) + else: + return 'M03' + + def dwell_code(self, p): + if p.dwelltime: + return 'G4 P' + str(p.dwelltime) + + def spindle_stop_code(self,p): + return 'M05' diff --git a/postprocessors/Toolchange_Probe_general.py b/postprocessors/Toolchange_Probe_general.py new file mode 100644 index 00000000..a676476d --- /dev/null +++ b/postprocessors/Toolchange_Probe_general.py @@ -0,0 +1,191 @@ +from FlatCAMPostProc import * + + +class Toolchange_Probe_general(FlatCAMPostProc): + + coordinate_format = "%.*f" + feedrate_format = '%.*f' + + def start_code(self, p): + units = ' ' + str(p['units']).lower() + coords_xy = p['toolchange_xy'] + gcode = '' + + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + + if str(p['options']['type']) == 'Geometry': + gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + + gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' + + if str(p['options']['type']) == 'Geometry': + gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n' + + gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' + gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n' + + if str(p['options']['type']) == 'Geometry': + if p['multidepth'] is True: + gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \ + str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n' + + gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' + gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' + + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' + + gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' + gcode += '(Z End: ' + str(p['endz']) + units + ')\n' + gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' + + if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + else: + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' + + gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) + + gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') + gcode += 'G90\n' + gcode += 'G17\n' + gcode += 'G94\n' + + return gcode + + def startz_code(self, p): + return '' + + def lift_code(self, p): + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + + def down_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + + def toolchange_code(self, p): + toolchangez = p.toolchangez + toolchangexy = p.toolchange_xy + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + + no_drills = 1 + + if int(p.tool) == 1 and p.startz is not None: + toolchangez = p.startz + + if p.units.upper() == 'MM': + toolC_formatted = format(p.toolC, '.2f') + else: + toolC_formatted = format(p.toolC, '.4f') + + if str(p['options']['type']) == 'Excellon': + for i in p['options']['Tools_in_use']: + if i[0] == p.tool: + no_drills = i[2] + + if toolchangexy is not None: + gcode = """ +G00 X{toolchangex} Y{toolchangey} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0 +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + else: + gcode = """ +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0 +""".format(tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + else: + if toolchangexy is not None: + gcode = """ +G00 X{toolchangex} Y{toolchangey} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC}) +M0 +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """ +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC}) +M0""".format(tool=int(p.tool), + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + def up_to_zero_code(self, p): + return 'G01 Z0' + + def position_code(self, p): + return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \ + (p.coords_decimals, p.x, p.coords_decimals, p.y) + + def rapid_code(self, p): + return ('G00 ' + self.position_code(p)).format(**p) + + def linear_code(self, p): + return ('G01 ' + self.position_code(p)).format(**p) + + def end_code(self, p): + coords_xy = p['toolchange_xy'] + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + return gcode + + def feedrate_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate)) + + def feedrate_z_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z)) + + def spindle_code(self, p): + if p.spindlespeed: + return 'M03 S' + str(p.spindlespeed) + else: + return 'M03' + + def dwell_code(self, p): + if p.dwelltime: + return 'G4 P' + str(p.dwelltime) + + def spindle_stop_code(self,p): + return 'M05' diff --git a/postprocessors/manual_toolchange.py b/postprocessors/Toolchange_manual.py similarity index 65% rename from postprocessors/manual_toolchange.py rename to postprocessors/Toolchange_manual.py index 64e65e77..f708ab3d 100644 --- a/postprocessors/manual_toolchange.py +++ b/postprocessors/Toolchange_manual.py @@ -1,7 +1,7 @@ from FlatCAMPostProc import * -class manual_toolchange(FlatCAMPostProc): +class Toolchange_manual(FlatCAMPostProc): coordinate_format = "%.*f" feedrate_format = '%.*f' @@ -11,6 +11,11 @@ class manual_toolchange(FlatCAMPostProc): coords_xy = p['toolchange_xy'] gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + if str(p['options']['type']) == 'Geometry': gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' @@ -29,20 +34,27 @@ class manual_toolchange(FlatCAMPostProc): gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' - gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' gcode += '(Z End: ' + str(p['endz']) + units + ')\n' gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': - gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n' else: - gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') gcode += 'G90\n' + gcode += 'G17\n' gcode += 'G94\n' return gcode @@ -62,8 +74,15 @@ class manual_toolchange(FlatCAMPostProc): def toolchange_code(self, p): toolchangez = p.toolchangez toolchangexy = p.toolchange_xy - toolchangex = toolchangexy[0] - toolchangey = toolchangexy[1] + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + # else: + # toolchangex = p.oldx + # toolchangey = p.oldy no_drills = 1 @@ -79,11 +98,13 @@ class manual_toolchange(FlatCAMPostProc): for i in p['options']['Tools_in_use']: if i[0] == p.tool: no_drills = i[2] - return """G00 Z{toolchangez} + + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} T{tool} -M5 -G00 X{toolchangex} Y{toolchangey} -(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}) +M5 +G00 X{toolchangex} Y{toolchangey} +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) M0 G01 Z0 M0 @@ -95,8 +116,30 @@ M0 tool=int(p.tool), t_drills=no_drills, toolC=toolC_formatted) + + else: + gcode = """G00 Z{toolchangez} +T{tool} +M5 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0 +G01 Z0 +M0 +G00 Z{toolchangez} +M0 +""".format( + toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + else: - return """G00 Z{toolchangez} + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} T{tool} M5 G00 X{toolchangex}Y{toolchangey} @@ -111,6 +154,23 @@ M0 toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), tool=int(p.tool), toolC=toolC_formatted) + else: + gcode = """G00 Z{toolchangez} +T{tool} +M5 +(MSG, Change to Tool Dia = {toolC}) +M0 +G01 Z0 +M0 +G00 Z{toolchangez} +M0 +""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode def up_to_zero_code(self, p): return 'G01 Z0' @@ -128,7 +188,10 @@ M0 def end_code(self, p): coords_xy = p['toolchange_xy'] gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") - gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + else: + gcode += 'G00 X0 Y0' + "\n" return gcode def feedrate_code(self, p): diff --git a/postprocessors/default.py b/postprocessors/default.py index 7beae193..e7d1d8e1 100644 --- a/postprocessors/default.py +++ b/postprocessors/default.py @@ -11,6 +11,11 @@ class default(FlatCAMPostProc): coords_xy = p['toolchange_xy'] gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + if str(p['options']['type']) == 'Geometry': gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' @@ -29,7 +34,12 @@ class default(FlatCAMPostProc): gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' - gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' + gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' gcode += '(Z End: ' + str(p['endz']) + units + ')\n' gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' @@ -37,7 +47,10 @@ class default(FlatCAMPostProc): if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' else: - gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) @@ -62,8 +75,12 @@ class default(FlatCAMPostProc): def toolchange_code(self, p): toolchangez = p.toolchangez toolchangexy = p.toolchange_xy - toolchangex = toolchangexy[0] - toolchangey = toolchangexy[1] + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] no_drills = 1 @@ -79,17 +96,49 @@ class default(FlatCAMPostProc): for i in p['options']['Tools_in_use']: if i[0] == p.tool: no_drills = i[2] - return """G00 Z{toolchangez} + + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} T{tool} M5 M6 -(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}) -M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), tool=int(p.tool), t_drills=no_drills, toolC=toolC_formatted) + else: + gcode = """G00 Z{toolchangez} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + else: - return """G00 Z{toolchangez} + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """G00 Z{toolchangez} T{tool} M5 M6 @@ -98,6 +147,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez) tool=int(p.tool), toolC=toolC_formatted) + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + def up_to_zero_code(self, p): return 'G01 Z0' @@ -114,7 +167,9 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez) def end_code(self, p): coords_xy = p['toolchange_xy'] gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") - gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" return gcode def feedrate_code(self, p): diff --git a/postprocessors/grbl_11.py b/postprocessors/grbl_11.py index 3b98d044..5470760f 100644 --- a/postprocessors/grbl_11.py +++ b/postprocessors/grbl_11.py @@ -11,6 +11,11 @@ class grbl_11(FlatCAMPostProc): coords_xy = p['toolchange_xy'] gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + if str(p['options']['type']) == 'Geometry': gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + '\n' @@ -29,7 +34,10 @@ class grbl_11(FlatCAMPostProc): gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' - gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' gcode += '(Z End: ' + str(p['endz']) + units + ')\n' gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' @@ -37,7 +45,10 @@ class grbl_11(FlatCAMPostProc): if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' else: - gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' gcode += '(Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + ')\n' + '\n' @@ -62,6 +73,15 @@ class grbl_11(FlatCAMPostProc): def toolchange_code(self, p): toolchangez = p.toolchangez + toolchangexy = p.toolchange_xy + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + + no_drills = 1 if int(p.tool) == 1 and p.startz is not None: toolchangez = p.startz @@ -75,17 +95,50 @@ class grbl_11(FlatCAMPostProc): for i in p['options']['Tools_in_use']: if i[0] == p.tool: no_drills = i[2] - return """G00 Z{toolchangez} + + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} T{tool} M5 M6 -(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}) -M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), tool=int(p.tool), t_drills=no_drills, toolC=toolC_formatted) + else: + gcode = """G00 Z{toolchangez} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + else: - return """G00 Z{toolchangez} + if toolchangexy is not None: + gcode = """G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +T{tool} +M5 +M6 +(MSG, Change to Tool Dia = {toolC}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """G00 Z{toolchangez} T{tool} M5 M6 @@ -94,6 +147,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez) tool=int(p.tool), toolC=toolC_formatted) + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + def up_to_zero_code(self, p): return 'G01 Z0' @@ -110,8 +167,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez) def end_code(self, p): coords_xy = p['toolchange_xy'] - gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.endz) + "\n") - gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" return gcode def feedrate_code(self, p): diff --git a/postprocessors/grbl_laser.py b/postprocessors/grbl_laser.py index 72611f00..ef83f92e 100644 --- a/postprocessors/grbl_laser.py +++ b/postprocessors/grbl_laser.py @@ -13,6 +13,11 @@ class grbl_laser(FlatCAMPostProc): units = ' ' + str(p['units']).lower() gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' @@ -22,7 +27,11 @@ class grbl_laser(FlatCAMPostProc): gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' else: gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' - gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' + gcode += 'G90\n' gcode += 'G94\n' gcode += 'G17\n' @@ -59,8 +68,11 @@ class grbl_laser(FlatCAMPostProc): ' F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate)) def end_code(self, p): + coords_xy = p['toolchange_xy'] gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") - gcode += 'G00 X0Y0' + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" return gcode def feedrate_code(self, p): diff --git a/postprocessors/line_xyz.py b/postprocessors/line_xyz.py index 2f01e492..a9db9a23 100644 --- a/postprocessors/line_xyz.py +++ b/postprocessors/line_xyz.py @@ -11,6 +11,11 @@ class line_xyz(FlatCAMPostProc): coords_xy = p['toolchange_xy'] gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + if str(p['options']['type']) == 'Geometry': gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' @@ -29,7 +34,10 @@ class line_xyz(FlatCAMPostProc): gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' - gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' gcode += '(Z End: ' + str(p['endz']) + units + ')\n' gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' @@ -37,7 +45,10 @@ class line_xyz(FlatCAMPostProc): if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' else: - gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) @@ -71,8 +82,19 @@ class line_xyz(FlatCAMPostProc): def toolchange_code(self, p): toolchangez = p.toolchangez toolchangexy = p.toolchange_xy - toolchangex = toolchangexy[0] - toolchangey = toolchangexy[1] + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + else: + if str(p['options']['type']) == 'Excellon': + toolchangex = p.oldx + toolchangey = p.oldy + else: + toolchangex = p.x + toolchangey = p.y no_drills = 1 @@ -88,19 +110,26 @@ class line_xyz(FlatCAMPostProc): for i in p['options']['Tools_in_use']: if i[0] == p.tool: no_drills = i[2] - return """G00 X{toolchangex} Y{toolchangey} Z{toolchangez} + gcode = """G00 X{toolchangex} Y{toolchangey} Z{toolchangez} T{tool} M5 M6 -(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}) +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex), toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), tool=int(p.tool), t_drills=no_drills, toolC=toolC_formatted) + + if f_plunge is True: + gcode += """\nG00 X{toolchangex} Y{toolchangey} Z{z_move}""".format( + toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move)) + return gcode else: - return """G00 X{toolchangex} Y{toolchangey} Z{toolchangez} + gcode = """G00 X{toolchangex} Y{toolchangey} Z{toolchangez} T{tool} M5 M6 @@ -111,6 +140,13 @@ M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex) tool=int(p.tool), toolC=toolC_formatted) + if f_plunge is True: + gcode += """\nG00 X{toolchangex} Y{toolchangey} Z{z_move}""".format( + toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + z_move=self.coordinate_format % (p.coords_decimals, p.z_move)) + return gcode + def up_to_zero_code(self, p): g = 'G01 ' + 'X' + self.coordinate_format % (p.coords_decimals, p.x) + \ ' Y' + self.coordinate_format % (p.coords_decimals, p.y) + \ @@ -132,7 +168,11 @@ M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex) return g def end_code(self, p): - g = ('G00 ' + self.position_code(p)).format(**p) + coords_xy = p['toolchange_xy'] + if coords_xy is not None: + g = 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + else: + g = ('G00 ' + self.position_code(p)).format(**p) g += ' Z' + self.coordinate_format % (p.coords_decimals, p.endz) return g diff --git a/postprocessors/marlin.py b/postprocessors/marlin.py index 57cb5be9..194e535c 100644 --- a/postprocessors/marlin.py +++ b/postprocessors/marlin.py @@ -9,8 +9,14 @@ class marlin(FlatCAMPostProc): def start_code(self, p): units = ' ' + str(p['units']).lower() + coords_xy = p['toolchange_xy'] gcode = '' + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + if str(p['options']['type']) == 'Geometry': gcode += ';TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + '\n' + '\n' @@ -29,6 +35,12 @@ class marlin(FlatCAMPostProc): gcode += ';Z_Move: ' + str(p['z_move']) + units + '\n' gcode += ';Z Toolchange: ' + str(p['toolchangez']) + units + '\n' + + if coords_xy is not None: + gcode += ';X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + '\n' + else: + gcode += ';X,Y Toolchange: ' + "None" + units + '\n' + gcode += ';Z Start: ' + str(p['startz']) + units + '\n' gcode += ';Z End: ' + str(p['endz']) + units + '\n' gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n' @@ -36,7 +48,10 @@ class marlin(FlatCAMPostProc): if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n' else: - gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n' + + gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n' + gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n' gcode += ';Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + '\n' + '\n' @@ -59,6 +74,14 @@ class marlin(FlatCAMPostProc): def toolchange_code(self, p): toolchangez = p.toolchangez + toolchangexy = p.toolchange_xy + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + no_drills = 1 if int(p.tool) == 1 and p.startz is not None: @@ -73,20 +96,61 @@ class marlin(FlatCAMPostProc): for i in p['options']['Tools_in_use']: if i[0] == p.tool: no_drills = i[2] - return """G0 Z{toolchangez} + + if toolchangexy is not None: + gcode = """G0 Z{toolchangez} +G0 X{toolchangex} Y{toolchangey} +T{tool} M5 -M0 Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills} -""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), - tool=int(p.tool), - t_drills=no_drills, - toolC=toolC_formatted) +M6 +;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills} +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + else: + gcode = """G0 Z{toolchangez} +T{tool} +M5 +M6 +;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills} +M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + else: - return """G0 Z{toolchangez} + if toolchangexy is not None: + gcode = """G0 Z{toolchangez} +G0 X{toolchangex} Y{toolchangey} +T{tool} M5 -M0 Change to Tool Dia = {toolC} -""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), - tool=int(p.tool), - toolC=toolC_formatted) +M6 +;MSG, Change to Tool Dia = {toolC} +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """G0 Z{toolchangez} +T{tool} +M5 +M6 +;MSG, Change to Tool Dia = {toolC} +M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode def up_to_zero_code(self, p): return 'G1 Z0' + " " + self.feedrate_code(p) @@ -104,7 +168,9 @@ M0 Change to Tool Dia = {toolC} def end_code(self, p): coords_xy = p['toolchange_xy'] gcode = ('G0 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + " " + self.feedrate_rapid_code(p) + "\n") - gcode += 'G0 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n" + + if coords_xy is not None: + gcode += 'G0 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n" return gcode diff --git a/share/fscreen32.png b/share/fscreen32.png new file mode 100644 index 00000000..061b8fd6 Binary files /dev/null and b/share/fscreen32.png differ diff --git a/share/plot32.png b/share/plot32.png new file mode 100644 index 00000000..8bab9afc Binary files /dev/null and b/share/plot32.png differ diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index 4a0cc2fc..48727c94 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -55,6 +55,9 @@ class TclCommandCncjob(TclCommandSignaled): ('multidepth', 'Use or not multidepth cnccut. (True or False)'), ('depthperpass', 'Height of one layer for multidepth.'), ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'), + ('toolchange', 'Enable tool changes (example: True).'), + ('toolchangez', 'Z distance for toolchange (example: 30.0).'), + ('toolchangexy', 'X, Y coordonates for toolchange in format (x, y) (example: (2.0, 3.1) ).'), ('endz', 'Height where the last move will park.'), ('outname', 'Name of the resulting Geometry object.'), ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive') @@ -96,6 +99,10 @@ class TclCommandCncjob(TclCommandSignaled): args["endz"]= args["endz"] if "endz" in args else obj.options["endz"] args["ppname_g"] = args["ppname_g"] if "ppname_g" in args else obj.options["ppname_g"] + args["toolchange"] = True if "toolchange" in args and args["toolchange"] == 1 else False + args["toolchangez"] = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"] + args["toolchangexy"] = args["toolchangexy"] if "toolchangexy" in args else obj.options["toolchangexy"] + del args['name'] # HACK !!! Should be solved elsewhere!!! diff --git a/tclCommands/TclCommandCutout.py b/tclCommands/TclCommandCutout.py index 4deb48ad..df40ab92 100644 --- a/tclCommands/TclCommandCutout.py +++ b/tclCommands/TclCommandCutout.py @@ -56,7 +56,7 @@ class TclCommandCutout(TclCommand): name = args['name'] else: self.app.inform.emit( - "[warning]The name of the object for which cutout is done is missing. Add it and retry.") + "[WARNING]The name of the object for which cutout is done is missing. Add it and retry.") return if 'margin' in args: diff --git a/tclCommands/TclCommandCutoutAny.py b/tclCommands/TclCommandCutoutAny.py index 548e1efa..49fd718c 100644 --- a/tclCommands/TclCommandCutoutAny.py +++ b/tclCommands/TclCommandCutoutAny.py @@ -61,7 +61,7 @@ class TclCommandCutoutAny(TclCommand): name = args['name'] else: self.app.inform.emit( - "[warning]The name of the object for which cutout is done is missing. Add it and retry.") + "[WARNING]The name of the object for which cutout is done is missing. Add it and retry.") return if 'margin' in args: @@ -91,11 +91,11 @@ class TclCommandCutoutAny(TclCommand): return "Could not retrieve object: %s" % name if 0 in {dia}: - self.app.inform.emit("[warning]Tool Diameter is zero value. Change it to a positive integer.") + self.app.inform.emit("[WARNING]Tool Diameter is zero value. Change it to a positive integer.") return "Tool Diameter is zero value. Change it to a positive integer." if gaps not in ['lr', 'tb', '2lr', '2tb', 4, 8]: - self.app.inform.emit("[warning]Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " + self.app.inform.emit("[WARNING]Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " "Fill in a correct value and retry. ") return @@ -129,7 +129,7 @@ class TclCommandCutoutAny(TclCommand): cutout_obj = self.app.collection.get_by_name(outname) else: - self.app.inform.emit("[error]Cancelled. Object type is not supported.") + self.app.inform.emit("[ERROR]Cancelled. Object type is not supported.") return try: diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index d8eda908..007fc6bc 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -47,6 +47,7 @@ class TclCommandDrillcncjob(TclCommandSignaled): ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), ('toolchange', 'Enable tool changes (example: True).'), ('toolchangez', 'Z distance for toolchange (example: 30.0).'), + ('toolchangexy', 'X, Y coordonates for toolchange in format (x, y) (example: (2.0, 3.1) ).'), ('endz', 'Z distance at job end (example: 30.0).'), ('ppname_e', 'This is the Excellon postprocessor name: case_sensitive, no_quotes'), ('outname', 'Name of the resulting Geometry object.'), @@ -85,7 +86,8 @@ class TclCommandDrillcncjob(TclCommandSignaled): drillz = 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.feedrate_rapid = args["feedrate_rapid"] if "feedrate_rapid" in args else obj.options["feedrate_rapid"] + job_obj.feedrate_rapid = args["feedrate_rapid"] \ + if "feedrate_rapid" in args else obj.options["feedrate_rapid"] job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None job_obj.pp_excellon_name = args["ppname_e"] if "ppname_e" in args \ @@ -93,13 +95,15 @@ class TclCommandDrillcncjob(TclCommandSignaled): toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False toolchangez = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"] + job_obj.toolchangexy = args["toolchangexy"] if "toolchangexy" in args else obj.options["toolchangexy"] endz = args["endz"] if "endz" in args else obj.options["endz"] tools = args["tools"] if "tools" in args else 'all' opt_type = args["opt_type"] if "opt_type" in args else 'B' - job_obj.generate_from_excellon_by_tool(obj, tools, drillz=drillz, toolchangez=toolchangez, endz=endz, + job_obj.generate_from_excellon_by_tool(obj, tools, drillz=drillz, toolchangez=toolchangez, + endz=endz, toolchange=toolchange, excellon_optimization_type=opt_type) job_obj.gcode_parse() job_obj.create_geometry() diff --git a/tclCommands/TclCommandListSys.py b/tclCommands/TclCommandListSys.py index 87d5a761..3fbda059 100644 --- a/tclCommands/TclCommandListSys.py +++ b/tclCommands/TclCommandListSys.py @@ -37,7 +37,7 @@ class TclCommandListSys(TclCommand): 'args': collections.OrderedDict([ ]), 'examples': ['list_sys', - 'list_sys ser' + 'list_sys ser', 'list_sys gerber', 'list_sys cncj'] } diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py index d03bef13..9472aa3e 100644 --- a/tclCommands/TclCommandOpenGerber.py +++ b/tclCommands/TclCommandOpenGerber.py @@ -57,12 +57,12 @@ class TclCommandOpenGerber(TclCommandSignaled): gerber_obj.parse_file(filename, follow=follow) except IOError: - app_obj.inform.emit("[error_notcl] Failed to open file: %s " % filename) + app_obj.inform.emit("[ERROR_NOTCL] 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_notcl] Failed to parse file: %s, %s " % (filename, str(e))) + app_obj.inform.emit("[ERROR_NOTCL] Failed to parse file: %s, %s " % (filename, str(e))) app_obj.progress.emit(0) self.log.error(str(e)) return diff --git a/tests/new_window_test.py b/tests/new_window_test.py new file mode 100644 index 00000000..442db9e7 --- /dev/null +++ b/tests/new_window_test.py @@ -0,0 +1,65 @@ +import sys +from PyQt5.Qt import * +from PyQt5 import QtGui, QtWidgets + +class MyPopup(QWidget): + def __init__(self): + QWidget.__init__(self) + lay = QtWidgets.QVBoxLayout() + self.setLayout(lay) + lay.setContentsMargins(0, 0, 0, 0) + le = QtWidgets.QLineEdit() + le.setText("Abracadabra") + le.setReadOnly(True) + # le.setStyleSheet("QLineEdit { qproperty-frame: false }") + le.setFrame(False) + le.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + # lay.addStretch() + but = QtWidgets.QPushButton("OK") + hlay = QtWidgets.QHBoxLayout() + hlay.setContentsMargins(0, 5, 5, 5) + + hlay.addStretch() + hlay.addWidget(but) + + lay.addWidget(le) + lay.addLayout(hlay) + # def paintEvent(self, e): + # dc = QtGui.QPainter(self) + # dc.drawLine(0, 0, 100, 100) + # dc.drawLine(100, 0, 0, 100) + +class MainWindow(QMainWindow): + def __init__(self, *args): + QtWidgets.QMainWindow.__init__(self, *args) + self.cw = QtWidgets.QWidget(self) + self.setCentralWidget(self.cw) + self.btn1 = QtWidgets.QPushButton("Click me", self.cw) + self.btn1.setGeometry(QRect(0, 0, 100, 30)) + self.btn1.clicked.connect(self.doit) + self.w = None + + def doit(self): + print("Opening a new popup window...") + self.w = MyPopup() + self.w.setGeometry(QRect(100, 100, 400, 200)) + self.w.show() + +class App(QApplication): + def __init__(self, *args): + QtWidgets.QApplication.__init__(self, *args) + self.main = MainWindow() + # self.lastWindowClosed.connect(self.byebye) + self.main.show() + + def byebye(self): + self.exit(0) + +def main(args): + global app + app = App(args) + app.exec_() + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file