# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # # Author: Juan Pablo Caram (c) # # Date: 2/5/2014 # # MIT Licence # # ########################################################## # ########################################################## # File modified by: Marius Stanciu # # ########################################################## from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString, LinearRing import shapely.affinity as affinity from camlib import Geometry from appObjects.FlatCAMObj import * import ezdxf import math import numpy as np from copy import deepcopy import traceback import gettext import appTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext class GeometryObject(FlatCAMObj, Geometry): """ Geometric object not associated with a specific format. """ optionChanged = QtCore.pyqtSignal(str) ui_type = GeometryObjectUI def __init__(self, name): self.decimals = self.app.decimals self.circle_steps = int(self.app.defaults["geometry_circle_steps"]) FlatCAMObj.__init__(self, name) Geometry.__init__(self, geo_steps_per_circle=self.circle_steps) self.kind = "geometry" self.options.update({ "plot": True, "multicolored": False, "cutz": -0.002, "vtipdia": 0.1, "vtipangle": 30, "travelz": 0.1, "feedrate": 5.0, "feedrate_z": 5.0, "feedrate_rapid": 5.0, "spindlespeed": 0, "dwell": True, "dwelltime": 1000, "multidepth": False, "depthperpass": 0.002, "extracut": False, "extracut_length": 0.1, "endz": 2.0, "endxy": '', "area_exclusion": False, "area_shape": "polygon", "area_strategy": "over", "area_overz": 1.0, "startz": None, "toolchange": False, "toolchangez": 1.0, "toolchangexy": "0.0, 0.0", "ppname_g": 'default', "z_pdepth": -0.02, "feedrate_probe": 3.0, }) if "cnctooldia" not in self.options: if type(self.app.defaults["geometry_cnctooldia"]) == float: self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"] else: try: tools_string = self.app.defaults["geometry_cnctooldia"].split(",") tools_diameters = [eval(a) for a in tools_string if a != ''] self.options["cnctooldia"] = tools_diameters[0] if tools_diameters else 0.0 except Exception as e: log.debug("FlatCAMObj.GeometryObject.init() --> %s" % str(e)) self.options["startz"] = self.app.defaults["geometry_startz"] # this will hold the tool unique ID that is useful when having multiple tools with same diameter self.tooluid = 0 ''' self.tools = {} This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the tool_id of the tools and the value is another dict that will hold the data under the following form: {tooluid: { 'tooldia': 1, 'offset': 'Path', 'offset_value': 0.0 'type': 'Rough', 'tool_type': 'C1', 'data': self.default_tool_data 'solid_geometry': [] } } ''' self.tools = {} # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table # those elements are the ones used for generating GCode self.sel_tools = {} self.offset_item_options = ["Path", "In", "Out", "Custom"] self.type_item_options = [_("Iso"), _("Rough"), _("Finish")] self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"] # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table self.v_tool_type = None # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry # the default value is False self.multigeo = False # flag to store if the geometry is part of a special group of geometries that can't be processed by the default # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries. self.special_group = None self.old_pp_state = self.app.defaults["geometry_multidepth"] self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"] self.units_found = self.app.defaults['units'] # this variable can be updated by the Object that generates the geometry self.tool_type = 'C1' # save here the old value for the Cut Z before it is changed by selecting a V-shape type tool in the tool table self.old_cutz = self.app.defaults["geometry_cutz"] self.fill_color = self.app.defaults['geometry_plot_line'] self.outline_color = self.app.defaults['geometry_plot_line'] self.alpha_level = 'FF' self.param_fields = {} # store here the state of the exclusion checkbox state to be restored after building the UI # TODO add this in the sel.app.defaults dict and in Preferences self.exclusion_area_cb_is_checked = False # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs += ['options', 'kind', 'tools', 'multigeo'] def build_ui(self): self.ui_disconnect() FlatCAMObj.build_ui(self) # Area Exception - exclusion shape added signal # first disconnect it from any other object try: self.app.exc_areas.e_shape_modified.disconnect() except (TypeError, AttributeError): pass # then connect it to the current build_ui() method self.app.exc_areas.e_shape_modified.connect(self.update_exclusion_table) self.units = self.app.defaults['units'] tool_idx = 0 n = len(self.tools) self.ui.geo_tools_table.setRowCount(n) for tooluid_key, tooluid_value in self.tools.items(): tool_idx += 1 row_no = tool_idx - 1 tool_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx)) tool_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.geo_tools_table.setItem(row_no, 0, tool_id) # Tool name/id # Make sure that the tool diameter when in MM is with no more than 2 decimals. # There are no tool bits in MM with more than 3 decimals diameter. # For INCH the decimals should be no more than 3. There are no tools under 10mils. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia']))) dia_item.setFlags(QtCore.Qt.ItemIsEnabled) offset_item = FCComboBox() for item in self.offset_item_options: offset_item.addItem(item) # offset_item.setStyleSheet('background-color: rgb(255,255,255)') idx = offset_item.findText(tooluid_value['offset']) offset_item.setCurrentIndex(idx) type_item = FCComboBox() for item in self.type_item_options: type_item.addItem(item) # type_item.setStyleSheet('background-color: rgb(255,255,255)') idx = type_item.findText(tooluid_value['type']) type_item.setCurrentIndex(idx) tool_type_item = FCComboBox() for item in self.tool_type_item_options: tool_type_item.addItem(item) # tool_type_item.setStyleSheet('background-color: rgb(255,255,255)') idx = tool_type_item.findText(tooluid_value['tool_type']) tool_type_item.setCurrentIndex(idx) tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key)) plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.RightToLeft) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) self.ui.geo_tools_table.setItem(row_no, 1, dia_item) # Diameter self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item) self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item) self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item) # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ### self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item) try: self.ui.tool_offset_entry.set_value(tooluid_value['offset_value']) except Exception as e: log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e)) # make the diameter column editable for row in range(tool_idx): self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled) # sort the tool diameter column # self.ui.geo_tools_table.sortItems(1) # all the tools are selected by default # self.ui.geo_tools_table.selectColumn(0) self.ui.geo_tools_table.resizeColumnsToContents() self.ui.geo_tools_table.resizeRowsToContents() vertical_header = self.ui.geo_tools_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) horizontal_header = self.ui.geo_tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 40) horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 17) # horizontal_header.setStretchLastSection(True) self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.geo_tools_table.setColumnWidth(0, 20) self.ui.geo_tools_table.setColumnWidth(4, 40) self.ui.geo_tools_table.setColumnWidth(6, 17) # self.ui.geo_tools_table.setSortingEnabled(True) self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight()) self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight()) # update UI for all rows - useful after units conversion but only if there is at least one row row_cnt = self.ui.geo_tools_table.rowCount() if row_cnt > 0: for r in range(row_cnt): self.update_ui(r) # select only the first tool / row selected_row = 0 try: self.select_tools_table_row(selected_row, clearsel=True) # update the Geometry UI self.update_ui() except Exception as e: # when the tools table is empty there will be this error but once the table is populated it will go away log.debug(str(e)) # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed # and can create some problems if self.multigeo is False: self.ui.geo_tools_table.setColumnHidden(6, True) else: self.ui.geo_tools_table.setColumnHidden(6, False) self.set_tool_offset_visibility(selected_row) # ----------------------------- # Build Exclusion Areas section # ----------------------------- e_len = len(self.app.exc_areas.exclusion_areas_storage) self.ui.exclusion_table.setRowCount(e_len) area_id = 0 for area in range(e_len): area_id += 1 area_dict = self.app.exc_areas.exclusion_areas_storage[area] area_id_item = QtWidgets.QTableWidgetItem('%d' % int(area_id)) area_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.exclusion_table.setItem(area, 0, area_id_item) # Area id object_item = QtWidgets.QTableWidgetItem('%s' % area_dict["obj_type"]) object_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.exclusion_table.setItem(area, 1, object_item) # Origin Object strategy_item = QtWidgets.QTableWidgetItem('%s' % area_dict["strategy"]) strategy_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.exclusion_table.setItem(area, 2, strategy_item) # Strategy overz_item = QtWidgets.QTableWidgetItem('%s' % area_dict["overz"]) overz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.exclusion_table.setItem(area, 3, overz_item) # Over Z self.ui.exclusion_table.resizeColumnsToContents() self.ui.exclusion_table.resizeRowsToContents() area_vheader = self.ui.exclusion_table.verticalHeader() area_vheader.hide() self.ui.exclusion_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) area_hheader = self.ui.exclusion_table.horizontalHeader() area_hheader.setMinimumSectionSize(10) area_hheader.setDefaultSectionSize(70) area_hheader.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) area_hheader.resizeSection(0, 20) area_hheader.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) area_hheader.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) area_hheader.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) # area_hheader.setStretchLastSection(True) self.ui.exclusion_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.exclusion_table.setColumnWidth(0, 20) self.ui.exclusion_table.setMinimumHeight(self.ui.exclusion_table.getHeight()) self.ui.exclusion_table.setMaximumHeight(self.ui.exclusion_table.getHeight()) # End Build Exclusion Areas # ----------------------------- # HACK: for whatever reasons the name in Selected tab is reverted to the original one after a successful rename # done in the collection view but only for Geometry objects. Perhaps some references remains. Should be fixed. self.ui.name_entry.set_value(self.options['name']) self.ui_connect() self.ui.e_cut_entry.setDisabled(False) if self.ui.extracut_cb.get_value() else \ self.ui.e_cut_entry.setDisabled(True) # set the text on tool_data_label after loading the object sel_rows = [] sel_items = self.ui.geo_tools_table.selectedItems() for it in sel_items: sel_rows.append(it.row()) if len(sel_rows) > 1: self.ui.tool_data_label.setText( "%s: %s" % (_('Parameters for'), _("Multiple Tools")) ) def set_ui(self, ui): FlatCAMObj.set_ui(self, ui) log.debug("GeometryObject.set_ui()") assert isinstance(self.ui, GeometryObjectUI), \ "Expected a GeometryObjectUI, got %s" % type(self.ui) self.units = self.app.defaults['units'].upper() self.units_found = self.app.defaults['units'] # populate preprocessor names in the combobox for name in list(self.app.preprocessors.keys()): self.ui.pp_geometry_name_cb.addItem(name) self.form_fields.update({ "plot": self.ui.plot_cb, "multicolored": self.ui.multicolored_cb, "cutz": self.ui.cutz_entry, "vtipdia": self.ui.tipdia_entry, "vtipangle": self.ui.tipangle_entry, "travelz": self.ui.travelz_entry, "feedrate": self.ui.cncfeedrate_entry, "feedrate_z": self.ui.feedrate_z_entry, "feedrate_rapid": self.ui.feedrate_rapid_entry, "spindlespeed": self.ui.cncspindlespeed_entry, "dwell": self.ui.dwell_cb, "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, "extracut_length": self.ui.e_cut_entry, "toolchange": self.ui.toolchangeg_cb, "toolchangez": self.ui.toolchangez_entry, "endz": self.ui.endz_entry, "endxy": self.ui.endxy_entry, "cnctooldia": self.ui.addtool_entry, "area_exclusion": self.ui.exclusion_cb, "area_shape": self.ui.area_shape_radio, "area_strategy": self.ui.strategy_radio, "area_overz": self.ui.over_z_entry, }) self.param_fields.update({ "vtipdia": self.ui.tipdia_entry, "vtipangle": self.ui.tipangle_entry, "cutz": self.ui.cutz_entry, "depthperpass": self.ui.maxdepth_entry, "multidepth": self.ui.mpass_cb, "travelz": self.ui.travelz_entry, "feedrate": self.ui.cncfeedrate_entry, "feedrate_z": self.ui.feedrate_z_entry, "feedrate_rapid": self.ui.feedrate_rapid_entry, "extracut": self.ui.extracut_cb, "extracut_length": self.ui.e_cut_entry, "spindlespeed": self.ui.cncspindlespeed_entry, "dwelltime": self.ui.dwelltime_entry, "dwell": self.ui.dwell_cb, "pdepth": self.ui.pdepth_entry, "pfeedrate": self.ui.feedrate_probe_entry, }) # Fill form fields only on object create self.to_form() # update the changes in UI depending on the selected preprocessor in Preferences # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the # self.ui.pp_geometry_name_cb combobox self.on_pp_changed() self.ui.tipdialabel.hide() self.ui.tipdia_entry.hide() self.ui.tipanglelabel.hide() self.ui.tipangle_entry.hide() self.ui.cutz_entry.setDisabled(False) # store here the default data for Geometry Data self.default_data = {} self.default_data.update({ "name": None, "plot": None, "cutz": None, "vtipdia": None, "vtipangle": None, "travelz": None, "feedrate": None, "feedrate_z": None, "feedrate_rapid": None, "dwell": None, "dwelltime": None, "multidepth": None, "ppname_g": None, "depthperpass": None, "extracut": None, "extracut_length": None, "toolchange": None, "toolchangez": None, "endz": None, "endxy": '', "area_exclusion": None, "area_shape": None, "area_strategy": None, "area_overz": None, "spindlespeed": 0, "toolchangexy": None, "startz": None }) # fill in self.default_data values from self.options for def_key in self.default_data: for opt_key, opt_val in self.options.items(): if def_key == opt_key: self.default_data[def_key] = deepcopy(opt_val) if type(self.options["cnctooldia"]) == float: tools_list = [self.options["cnctooldia"]] else: try: temp_tools = self.options["cnctooldia"].split(",") tools_list = [ float(eval(dia)) for dia in temp_tools if dia != '' ] except Exception as e: log.error("GeometryObject.set_ui() -> At least one tool diameter needed. " "Verify in Edit -> Preferences -> Geometry General -> Tool dia. %s" % str(e)) return self.tooluid += 1 if not self.tools: for toold in tools_list: new_data = deepcopy(self.default_data) self.tools.update({ self.tooluid: { 'tooldia': float('%.*f' % (self.decimals, float(toold))), 'offset': 'Path', 'offset_value': 0.0, 'type': _('Rough'), 'tool_type': self.tool_type, 'data': new_data, 'solid_geometry': self.solid_geometry } }) self.tooluid += 1 else: # if self.tools is not empty then it can safely be assumed that it comes from an opened project. # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are # again float type; dict's don't like having keys changed when iterated through therefore the need for the # following convoluted way of changing the keys from string to float type temp_tools = {} for tooluid_key in self.tools: val = deepcopy(self.tools[tooluid_key]) new_key = deepcopy(int(tooluid_key)) temp_tools[new_key] = val self.tools.clear() self.tools = deepcopy(temp_tools) self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # used to store the state of the mpass_cb if the selected preprocessor for geometry is hpgl self.old_pp_state = self.default_data['multidepth'] self.old_toolchangeg_state = self.default_data['toolchange'] if not isinstance(self.ui, GeometryObjectUI): log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui)) return self.ui.geo_tools_table.setupContextMenu() self.ui.geo_tools_table.addContextMenu( _("Add from Tool DB"), self.on_tool_add_from_db_clicked, icon=QtGui.QIcon(self.app.resource_location + "/plus16.png")) self.ui.geo_tools_table.addContextMenu( _("Copy"), self.on_tool_copy, icon=QtGui.QIcon(self.app.resource_location + "/copy16.png")) self.ui.geo_tools_table.addContextMenu( _("Delete"), lambda: self.on_tool_delete(all_tools=None), icon=QtGui.QIcon(self.app.resource_location + "/delete32.png")) # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText('%s' % _('Basic')) self.ui.geo_tools_table.setColumnHidden(2, True) self.ui.geo_tools_table.setColumnHidden(3, True) # self.ui.geo_tools_table.setColumnHidden(4, True) self.ui.addtool_entry_lbl.hide() self.ui.addtool_entry.hide() self.ui.addtool_btn.hide() self.ui.copytool_btn.hide() self.ui.deltool_btn.hide() # self.ui.endz_label.hide() # self.ui.endz_entry.hide() self.ui.fr_rapidlabel.hide() self.ui.feedrate_rapid_entry.hide() self.ui.extracut_cb.hide() self.ui.e_cut_entry.hide() self.ui.pdepth_label.hide() self.ui.pdepth_entry.hide() self.ui.feedrate_probe_label.hide() self.ui.feedrate_probe_entry.hide() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.e_cut_entry.setDisabled(False) if self.app.defaults['geometry_extracut'] else \ self.ui.e_cut_entry.setDisabled(True) self.ui.extracut_cb.toggled.connect(lambda state: self.ui.e_cut_entry.setDisabled(not state)) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click) self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False)) self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False)) self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed) self.ui.tipdia_entry.valueChanged.connect(self.update_cutz) self.ui.tipangle_entry.valueChanged.connect(self.update_cutz) self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked) self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked) self.ui.cutz_entry.returnPressed.connect(self.on_cut_z_changed) # Exclusion areas signals self.ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all) self.ui.exclusion_table.lost_focus.connect(self.clear_selection) self.ui.exclusion_table.itemClicked.connect(self.draw_sel_shape) self.ui.add_area_button.clicked.connect(self.on_add_area_click) self.ui.delete_area_button.clicked.connect(self.on_clear_area_click) self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas) self.ui.strategy_radio.activated_custom.connect(self.on_strategy) def on_cut_z_changed(self): self.old_cutz = self.ui.cutz_entry.get_value() def set_tool_offset_visibility(self, current_row): if current_row is None: return try: tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2) if tool_offset is not None: tool_offset_txt = tool_offset.currentText() if tool_offset_txt == 'Custom': self.ui.tool_offset_entry.show() self.ui.tool_offset_lbl.show() else: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() except Exception as e: log.debug("set_tool_offset_visibility() --> " + str(e)) return def on_offset_value_edited(self): """ This will save the offset_value into self.tools storage whenever the offset value is edited :return: """ for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tool_uid: 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] %s' % _("Wrong value format entered, use a number.")) return def ui_connect(self): # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the # changes in geometry UI for i in self.param_fields: current_widget = self.param_fields[i] if isinstance(current_widget, FCCheckBox): current_widget.stateChanged.connect(self.gui_form_to_storage) elif isinstance(current_widget, FCComboBox): current_widget.currentIndexChanged.connect(self.gui_form_to_storage) elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \ isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry): current_widget.editingFinished.connect(self.gui_form_to_storage) elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner): current_widget.returnPressed.connect(self.gui_form_to_storage) for row in range(self.ui.geo_tools_table.rowCount()): for col in [2, 3, 4]: self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect( self.on_tooltable_cellwidget_change) # I use lambda's because the connected functions have parameters that could be used in certain scenarios self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add()) self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy()) self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete()) # self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change) self.ui.geo_tools_table.clicked.connect(self.on_row_selection_change) self.ui.geo_tools_table.horizontalHeader().sectionClicked.connect(self.on_row_selection_change) self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit) self.ui.tool_offset_entry.returnPressed.connect(self.on_offset_value_edited) for row in range(self.ui.geo_tools_table.rowCount()): self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) # common parameters update self.ui.pp_geometry_name_cb.currentIndexChanged.connect(self.update_common_param_in_storage) def ui_disconnect(self): # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the # changes in geometry UI for i in self.param_fields: # current_widget = self.ui.grid3.itemAt(i).widget() current_widget = self.param_fields[i] if isinstance(current_widget, FCCheckBox): try: current_widget.stateChanged.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass elif isinstance(current_widget, FCComboBox): try: current_widget.currentIndexChanged.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass elif isinstance(current_widget, LengthEntry) or isinstance(current_widget, IntEntry) or \ isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry): try: current_widget.editingFinished.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner): try: current_widget.returnPressed.disconnect(self.gui_form_to_storage) except TypeError: pass for row in range(self.ui.geo_tools_table.rowCount()): for col in [2, 3, 4]: try: self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect() except (TypeError, AttributeError): pass try: self.ui.addtool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.copytool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.deltool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.geo_tools_table.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.geo_tools_table.horizontalHeader().sectionClicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.geo_tools_table.itemChanged.disconnect() except (TypeError, AttributeError): pass try: self.ui.tool_offset_entry.returnPressed.disconnect() except (TypeError, AttributeError): pass for row in range(self.ui.geo_tools_table.rowCount()): try: self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.plot_cb.stateChanged.disconnect() except (TypeError, AttributeError): pass def on_row_selection_change(self): self.update_ui() def update_ui(self, row=None): self.ui_disconnect() if row is None: sel_rows = [] sel_items = self.ui.geo_tools_table.selectedItems() for it in sel_items: sel_rows.append(it.row()) else: sel_rows = row if type(row) == list else [row] if not sel_rows: sel_rows = [0] for current_row in sel_rows: self.set_tool_offset_visibility(current_row) # populate the form with the data from the tool associated with the row parameter try: item = self.ui.geo_tools_table.item(current_row, 5) if type(item) is not None: tooluid = int(item.text()) else: self.ui_connect() return except Exception as e: log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e)) self.ui_connect() return # update the QLabel that shows for which Tool we have the parameters in the UI form if len(sel_rows) == 1: self.ui.tool_data_label.setText( "%s: %s %d" % (_('Parameters for'), _("Tool"), tooluid) ) # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields try: item = self.ui.geo_tools_table.cellWidget(current_row, 4) if item is not None: tool_type_txt = item.currentText() self.ui_update_v_shape(tool_type_txt=tool_type_txt) else: self.ui_connect() return except Exception as e: log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e)) return try: # set the form with data from the newly selected tool for tooluid_key, tooluid_value in list(self.tools.items()): if int(tooluid_key) == tooluid: for key, value in list(tooluid_value.items()): if key == 'data': form_value_storage = tooluid_value['data'] self.update_form(form_value_storage) if key == 'offset_value': # update the offset value in the entry even if the entry is hidden self.ui.tool_offset_entry.set_value(tooluid_value['offset_value']) if key == 'tool_type' and value == 'V': self.update_cutz() except Exception as e: log.debug("GeometryObject.update_ui() -> %s " % str(e)) else: self.ui.tool_data_label.setText( "%s: %s" % (_('Parameters for'), _("Multiple Tools")) ) self.ui_connect() def on_tool_add(self, dia=None): self.ui_disconnect() self.units = self.app.defaults['units'].upper() if dia is not None: tooldia = dia else: tooldia = float(self.ui.addtool_entry.get_value()) # construct a list of all 'tooluid' in the self.tools # tool_uid_list = [] # for tooluid_key in self.tools: # tool_uid_list.append(int(tooluid_key)) tool_uid_list = [int(tooluid_key) for tooluid_key in self.tools] # find maximum from the temp_uid, add 1 and this is the new 'tooluid' max_uid = max(tool_uid_list) if tool_uid_list else 0 self.tooluid = max_uid + 1 tooldia = float('%.*f' % (self.decimals, tooldia)) # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data # otherwise we add a tool with data copied from last tool if self.tools: last_data = self.tools[max_uid]['data'] last_offset = self.tools[max_uid]['offset'] last_offset_value = self.tools[max_uid]['offset_value'] last_type = self.tools[max_uid]['type'] 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, 'offset': last_offset, 'offset_value': last_offset_value, 'type': last_type, 'tool_type': last_tool_type, 'data': deepcopy(last_data), 'solid_geometry': deepcopy(last_solid_geometry) } }) else: self.tools.update({ self.tooluid: { 'tooldia': tooldia, 'offset': 'Path', 'offset_value': 0.0, 'type': _('Rough'), 'tool_type': 'C1', 'data': deepcopy(self.default_data), 'solid_geometry': self.solid_geometry } }) self.tools[self.tooluid]['data']['name'] = self.options['name'] self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except TypeError: pass self.ser_attrs.append('tools') self.app.inform.emit('[success] %s' % _("Tool added in Tool Table.")) self.ui_connect() self.build_ui() # if there is no tool left in the Tools Table, enable the parameters appGUI if self.ui.geo_tools_table.rowCount() != 0: self.ui.geo_param_frame.setDisabled(False) def on_tool_add_from_db_clicked(self): """ Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object and display the Tools Database tab in the form needed for the Tool adding :return: None """ # if the Tools Database is already opened focus on it for idx in range(self.app.ui.plot_tab_area.count()): if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"): self.app.ui.plot_tab_area.setCurrentWidget(self.app.tools_db_tab) break self.app.on_tools_database() self.app.tools_db_tab.ok_to_add = True self.app.tools_db_tab.buttons_frame.hide() self.app.tools_db_tab.add_tool_from_db.show() self.app.tools_db_tab.cancel_tool_from_db.show() def on_tool_from_db_inserted(self, tool): """ Called from the Tools DB object through a App method when adding a tool from Tools Database :param tool: a dict with the tool data :return: None """ self.ui_disconnect() self.units = self.app.defaults['units'].upper() tooldia = float(tool['tooldia']) # construct a list of all 'tooluid' in the self.tools tool_uid_list = [] for tooluid_key in self.tools: tool_uid_item = int(tooluid_key) tool_uid_list.append(tool_uid_item) # find maximum from the temp_uid, add 1 and this is the new 'tooluid' if not tool_uid_list: max_uid = 0 else: max_uid = max(tool_uid_list) self.tooluid = max_uid + 1 tooldia = float('%.*f' % (self.decimals, tooldia)) self.tools.update({ self.tooluid: { 'tooldia': tooldia, 'offset': tool['offset'], 'offset_value': float(tool['offset_value']), 'type': tool['type'], 'tool_type': tool['tool_type'], 'data': deepcopy(tool['data']), 'solid_geometry': self.solid_geometry } }) self.tools[self.tooluid]['data']['name'] = self.options['name'] self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except TypeError: pass self.ser_attrs.append('tools') self.ui_connect() self.build_ui() # if there is no tool left in the Tools Table, enable the parameters appGUI if self.ui.geo_tools_table.rowCount() != 0: self.ui.geo_param_frame.setDisabled(False) def on_tool_copy(self, all_tools=None): self.ui_disconnect() # find the tool_uid maximum value in the self.tools uid_list = [] for key in self.tools: uid_list.append(int(key)) try: max_uid = max(uid_list, key=int) except ValueError: max_uid = 0 if all_tools is None: if self.ui.geo_tools_table.selectedItems(): for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue try: 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)] = deepcopy(self.tools[tooluid_copy]) except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy.")) self.ui_connect() self.build_ui() return except Exception as e: log.debug("on_tool_copy() --> " + str(e)) # deselect the table # self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy.")) self.ui_connect() self.build_ui() return else: # we copy all tools in geo_tools_table try: temp_tools = deepcopy(self.tools) max_uid += 1 for tooluid in temp_tools: self.tools[int(max_uid)] = deepcopy(temp_tools[tooluid]) temp_tools.clear() except Exception as e: log.debug("on_tool_copy() --> " + str(e)) # if there are no more tools in geo tools table then hide the tool offset if not self.tools: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except ValueError: pass self.ser_attrs.append('tools') self.ui_connect() self.build_ui() self.app.inform.emit('[success] %s' % _("Tool was copied in Tool Table.")) def on_tool_edit(self, current_item): self.ui_disconnect() current_row = current_item.row() 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] %s' % _("Wrong value format entered, use a number.")) return tool_dia = float('%.*f' % (self.decimals, d)) tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text()) self.tools[tooluid]['tooldia'] = tool_dia try: self.ser_attrs.remove('tools') self.ser_attrs.append('tools') except (TypeError, ValueError): pass self.app.inform.emit('[success] %s' % _("Tool was edited in Tool Table.")) self.ui_connect() self.build_ui() def on_tool_delete(self, all_tools=None): self.ui_disconnect() if all_tools is None: if self.ui.geo_tools_table.selectedItems(): for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue try: tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) temp_tools = deepcopy(self.tools) for tooluid_key in self.tools: if int(tooluid_key) == tooluid_del: # if the self.tools has only one tool and we delete it then we move the solid_geometry # as a property of the object otherwise there will be nothing to hold it if len(self.tools) == 1: self.solid_geometry = deepcopy(self.tools[tooluid_key]['solid_geometry']) temp_tools.pop(tooluid_del, None) self.tools = deepcopy(temp_tools) temp_tools.clear() except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete.")) self.ui_connect() self.build_ui() return except Exception as e: log.debug("on_tool_delete() --> " + str(e)) # deselect the table # self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete.")) self.ui_connect() self.build_ui() return else: # we delete all tools in geo_tools_table self.tools.clear() self.app.plot_all() # if there are no more tools in geo tools table then hide the tool offset if not self.tools: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except TypeError: pass self.ser_attrs.append('tools') self.ui_connect() self.build_ui() self.app.inform.emit('[success] %s' % _("Tool was deleted in Tool Table.")) obj_active = self.app.collection.get_active() # if the object was MultiGeo and now it has no tool at all (therefore no geometry) # we make it back SingleGeo if self.ui.geo_tools_table.rowCount() <= 0: obj_active.multigeo = False obj_active.options['xmin'] = 0 obj_active.options['ymin'] = 0 obj_active.options['xmax'] = 0 obj_active.options['ymax'] = 0 if obj_active.multigeo is True: try: xmin, ymin, xmax, ymax = obj_active.bounds() obj_active.options['xmin'] = xmin obj_active.options['ymin'] = ymin obj_active.options['xmax'] = xmax obj_active.options['ymax'] = ymax except Exception: obj_active.options['xmin'] = 0 obj_active.options['ymin'] = 0 obj_active.options['xmax'] = 0 obj_active.options['ymax'] = 0 # if there is no tool left in the Tools Table, disable the parameters appGUI if self.ui.geo_tools_table.rowCount() == 0: self.ui.geo_param_frame.setDisabled(True) def ui_update_v_shape(self, tool_type_txt): if tool_type_txt == 'V': self.ui.tipdialabel.show() self.ui.tipdia_entry.show() self.ui.tipanglelabel.show() self.ui.tipangle_entry.show() self.ui.cutz_entry.setDisabled(True) self.ui.cutzlabel.setToolTip( _("Disabled because the tool is V-shape.\n" "For V-shape tools the depth of cut is\n" "calculated from other parameters like:\n" "- 'V-tip Angle' -> angle at the tip of the tool\n" "- 'V-tip Dia' -> diameter at the tip of the tool \n" "- Tool Dia -> 'Dia' column found in the Tool Table\n" "NB: a value of zero means that Tool Dia = 'V-tip Dia'") ) self.ui.cutz_entry.setToolTip( _("Disabled because the tool is V-shape.\n" "For V-shape tools the depth of cut is\n" "calculated from other parameters like:\n" "- 'V-tip Angle' -> angle at the tip of the tool\n" "- 'V-tip Dia' -> diameter at the tip of the tool \n" "- Tool Dia -> 'Dia' column found in the Tool Table\n" "NB: a value of zero means that Tool Dia = 'V-tip Dia'") ) self.update_cutz() else: self.ui.tipdialabel.hide() self.ui.tipdia_entry.hide() self.ui.tipanglelabel.hide() self.ui.tipangle_entry.hide() self.ui.cutz_entry.setDisabled(False) self.ui.cutzlabel.setToolTip( _("Cutting depth (negative)\n" "below the copper surface.") ) self.ui.cutz_entry.setToolTip('') def update_cutz(self): vdia = float(self.ui.tipdia_entry.get_value()) half_vangle = float(self.ui.tipangle_entry.get_value()) / 2 row = self.ui.geo_tools_table.currentRow() tool_uid_item = self.ui.geo_tools_table.item(row, 5) if tool_uid_item is None: return tool_uid = int(tool_uid_item.text()) tool_dia_item = self.ui.geo_tools_table.item(row, 1) if tool_dia_item is None: return tooldia = float(tool_dia_item.text()) try: new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle))) except ZeroDivisionError: new_cutz = self.old_cutz new_cutz = float('%.*f' % (self.decimals, new_cutz)) * -1.0 # this value has to be negative self.ui.cutz_entry.set_value(new_cutz) # store the new CutZ value into storage (self.tools) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tool_uid: tooluid_value['data']['cutz'] = new_cutz def on_tooltable_cellwidget_change(self): cw = self.sender() # assert isinstance(cw, FCComboBox) or isinstance(cw, FCCheckBox),\ # "Expected a FCCombobox or a FCCheckbox got %s" % type(cw) cw_index = self.ui.geo_tools_table.indexAt(cw.pos()) cw_row = cw_index.row() cw_col = cw_index.column() current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text()) # store the text of the cellWidget that changed it's index in the self.tools for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == current_uid: cb_txt = cw.currentText() if cw_col == 2: tooluid_value['offset'] = cb_txt if cb_txt == 'Custom': self.ui.tool_offset_entry.show() self.ui.tool_offset_lbl.show() else: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # reset the offset_value in storage self.tools tooluid_value['offset_value'] = 0.0 elif cw_col == 3: # force toolpath type as 'Iso' if the tool type is V-Shape if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V': tooluid_value['type'] = _('Iso') idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso')) self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx) else: tooluid_value['type'] = cb_txt elif cw_col == 4: tooluid_value['tool_type'] = cb_txt # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso if cb_txt == 'V': idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso')) self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx) else: self.ui.cutz_entry.set_value(self.old_cutz) self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText()) def update_form(self, dict_storage): for form_key in self.form_fields: for storage_key in dict_storage: if form_key == storage_key: try: self.form_fields[form_key].set_value(dict_storage[form_key]) except Exception as e: log.debug(str(e)) # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here self.ui.ois_dwell_geo.on_cb_change() self.ui.ois_mpass_geo.on_cb_change() self.ui.ois_tcz_geo.on_cb_change() def on_apply_param_to_all_clicked(self): if self.ui.geo_tools_table.rowCount() == 0: # there is no tool in tool table so we can't save the GUI elements values to storage log.debug("GeometryObject.gui_form_to_storage() --> no tool in Tools Table, aborting.") return self.ui_disconnect() row = self.ui.geo_tools_table.currentRow() if row < 0: row = 0 # store all the data associated with the row parameter to the self.tools storage tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text()) offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText() type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText() tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText() offset_value_item = float(self.ui.tool_offset_entry.get_value()) # this new dict will hold the actual useful data, another dict that is the value of key 'data' temp_tools = {} temp_dia = {} temp_data = {} for tooluid_key, tooluid_value in self.tools.items(): for key, value in tooluid_value.items(): if key == 'tooldia': temp_dia[key] = tooldia_item # update the 'offset', 'type' and 'tool_type' sections if key == 'offset': temp_dia[key] = offset_item if key == 'type': temp_dia[key] = type_item if key == 'tool_type': temp_dia[key] = tool_type_item if key == 'offset_value': temp_dia[key] = offset_value_item if key == 'data': # update the 'data' section for data_key in tooluid_value[key].keys(): for form_key, form_value in self.form_fields.items(): if form_key == data_key: temp_data[data_key] = form_value.get_value() # make sure we make a copy of the keys not in the form (we may use 'data' keys that are # updated from self.app.defaults if data_key not in self.form_fields: temp_data[data_key] = value[data_key] temp_dia[key] = deepcopy(temp_data) temp_data.clear() if key == 'solid_geometry': temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry']) temp_tools[tooluid_key] = deepcopy(temp_dia) self.tools.clear() self.tools = deepcopy(temp_tools) temp_tools.clear() self.ui_connect() def gui_form_to_storage(self): self.ui_disconnect() if self.ui.geo_tools_table.rowCount() == 0: # there is no tool in tool table so we can't save the GUI elements values to storage log.debug("GeometryObject.gui_form_to_storage() --> no tool in Tools Table, aborting.") return widget_changed = self.sender() try: widget_idx = self.ui.grid3.indexOf(widget_changed) except Exception as e: log.debug("GeometryObject.gui_form_to_storage() -- wdg index -> %s" % str(e)) return # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z if widget_idx == 1 or widget_idx == 3: self.update_cutz() # the original connect() function of the OptionalInputSelection is no longer working because of the # ui_diconnect() so I use this 'hack' if isinstance(widget_changed, FCCheckBox): if widget_changed.text() == 'Multi-Depth:': self.ui.ois_mpass_geo.on_cb_change() if widget_changed.text() == 'Tool change': self.ui.ois_tcz_geo.on_cb_change() if widget_changed.text() == 'Dwell:': self.ui.ois_dwell_geo.on_cb_change() row = self.ui.geo_tools_table.currentRow() if row < 0: row = 0 # store all the data associated with the row parameter to the self.tools storage tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text()) offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText() type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText() 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 = float(self.ui.tool_offset_entry.get_value()) # this new dict will hold the actual useful data, another dict that is the value of key 'data' temp_tools = {} temp_dia = {} temp_data = {} for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid_item: for key, value in tooluid_value.items(): if key == 'tooldia': temp_dia[key] = tooldia_item # update the 'offset', 'type' and 'tool_type' sections if key == 'offset': temp_dia[key] = offset_item if key == 'type': temp_dia[key] = type_item if key == 'tool_type': temp_dia[key] = tool_type_item if key == 'offset_value': temp_dia[key] = offset_value_item if key == 'data': # update the 'data' section for data_key in tooluid_value[key].keys(): for form_key, form_value in self.form_fields.items(): if form_key == data_key: temp_data[data_key] = form_value.get_value() # make sure we make a copy of the keys not in the form (we may use 'data' keys that are # updated from self.app.defaults if data_key not in self.form_fields: temp_data[data_key] = value[data_key] temp_dia[key] = deepcopy(temp_data) temp_data.clear() if key == 'solid_geometry': temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry']) temp_tools[tooluid_key] = deepcopy(temp_dia) else: temp_tools[tooluid_key] = deepcopy(tooluid_value) self.tools.clear() self.tools = deepcopy(temp_tools) temp_tools.clear() self.ui_connect() def update_common_param_in_storage(self): for tooluid_value in self.tools.values(): tooluid_value['data']['ppname_g'] = self.ui.pp_geometry_name_cb.get_value() def select_tools_table_row(self, row, clearsel=None): if clearsel: self.ui.geo_tools_table.clearSelection() if self.ui.geo_tools_table.rowCount() > 0: # self.ui.geo_tools_table.item(row, 0).setSelected(True) self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0)) def export_dxf(self): dwg = None try: dwg = ezdxf.new('R2010') msp = dwg.modelspace() def g2dxf(dxf_space, geo_obj): if isinstance(geo_obj, MultiPolygon): for poly in geo_obj: ext_points = list(poly.exterior.coords) dxf_space.add_lwpolyline(ext_points) for interior in poly.interiors: dxf_space.add_lwpolyline(list(interior.coords)) if isinstance(geo_obj, Polygon): ext_points = list(geo_obj.exterior.coords) dxf_space.add_lwpolyline(ext_points) for interior in geo_obj.interiors: dxf_space.add_lwpolyline(list(interior.coords)) if isinstance(geo_obj, MultiLineString): for line in geo_obj: dxf_space.add_lwpolyline(list(line.coords)) if isinstance(geo_obj, LineString) or isinstance(geo_obj, LinearRing): dxf_space.add_lwpolyline(list(geo_obj.coords)) multigeo_solid_geometry = [] if self.multigeo: for tool in self.tools: multigeo_solid_geometry += self.tools[tool]['solid_geometry'] else: multigeo_solid_geometry = self.solid_geometry for geo in multigeo_solid_geometry: if type(geo) == list: for g in geo: g2dxf(msp, g) else: g2dxf(msp, geo) # points = GeometryObject.get_pts(geo) # msp.add_lwpolyline(points) except Exception as e: log.debug(str(e)) return dwg def get_selected_tools_table_items(self): """ Returns a list of lists, each list in the list is made out of row elements :return: List of table_tools items. :rtype: list """ table_tools_items = [] if self.multigeo: for x in self.ui.geo_tools_table.selectedItems(): elem = [] txt = '' for column in range(0, self.ui.geo_tools_table.columnCount()): try: txt = self.ui.geo_tools_table.item(x.row(), column).text() except AttributeError: try: txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText() except AttributeError: pass elem.append(txt) table_tools_items.append(deepcopy(elem)) # table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text() # for column in range(0, self.ui.geo_tools_table.columnCount())]) else: for x in self.ui.geo_tools_table.selectedItems(): r = [] txt = '' # the last 2 columns for single-geo geometry are irrelevant and create problems reading # so we don't read them for column in range(0, self.ui.geo_tools_table.columnCount() - 2): # the columns have items that have text but also have items that are widgets # for which the text they hold has to be read differently try: txt = self.ui.geo_tools_table.item(x.row(), column).text() except AttributeError: try: txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText() except AttributeError: pass r.append(txt) table_tools_items.append(r) for item in table_tools_items: item[0] = str(item[0]) return table_tools_items def on_pp_changed(self): current_pp = self.ui.pp_geometry_name_cb.get_value() if current_pp == 'hpgl': self.old_pp_state = self.ui.mpass_cb.get_value() self.old_toolchangeg_state = self.ui.toolchangeg_cb.get_value() self.ui.mpass_cb.set_value(False) self.ui.mpass_cb.setDisabled(True) self.ui.toolchangeg_cb.set_value(True) self.ui.toolchangeg_cb.setDisabled(True) else: self.ui.mpass_cb.set_value(self.old_pp_state) self.ui.mpass_cb.setDisabled(False) 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() if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower(): self.ui.fr_rapidlabel.show() self.ui.feedrate_rapid_entry.show() else: self.ui.fr_rapidlabel.hide() self.ui.feedrate_rapid_entry.hide() if 'laser' in current_pp.lower(): self.ui.cutzlabel.hide() self.ui.cutz_entry.hide() try: self.ui.mpass_cb.hide() self.ui.maxdepth_entry.hide() except AttributeError: pass if 'marlin' in current_pp.lower(): self.ui.travelzlabel.setText('%s:' % _("Focus Z")) self.ui.endz_label.show() self.ui.endz_entry.show() else: self.ui.travelzlabel.hide() self.ui.travelz_entry.hide() self.ui.endz_label.hide() self.ui.endz_entry.hide() try: self.ui.frzlabel.hide() self.ui.feedrate_z_entry.hide() except AttributeError: pass self.ui.dwell_cb.hide() self.ui.dwelltime_entry.hide() self.ui.spindle_label.setText('%s:' % _("Laser Power")) try: self.ui.tool_offset_label.hide() self.ui.offset_entry.hide() except AttributeError: pass else: self.ui.cutzlabel.show() self.ui.cutz_entry.show() try: self.ui.mpass_cb.show() self.ui.maxdepth_entry.show() except AttributeError: pass self.ui.travelzlabel.setText('%s:' % _('Travel Z')) self.ui.travelzlabel.show() self.ui.travelz_entry.show() self.ui.endz_label.show() self.ui.endz_entry.show() try: self.ui.frzlabel.show() self.ui.feedrate_z_entry.show() except AttributeError: pass self.ui.dwell_cb.show() self.ui.dwelltime_entry.show() self.ui.spindle_label.setText('%s:' % _('Spindle speed')) try: self.ui.tool_offset_lbl.show() self.ui.offset_entry.show() except AttributeError: pass def on_generatecnc_button_click(self, *args): log.debug("Generating CNCJob from Geometry ...") self.app.defaults.report_usage("geometry_on_generatecnc_button") # this reads the values in the UI form to the self.options dictionary self.read_form() self.sel_tools = {} try: if self.special_group: self.app.inform.emit( '[WARNING_NOTCL] %s %s %s.' % (_("This Geometry can't be processed because it is"), str(self.special_group), _("geometry")) ) return except AttributeError: pass # 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(): # 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] %s' % # _("Wrong 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: deepcopy(tooluid_value) }) self.mtool_gen_cncjob() self.ui.geo_tools_table.clearSelection() elif self.ui.geo_tools_table.rowCount() == 1: tooluid = int(self.ui.geo_tools_table.item(0, 5).text()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid: self.sel_tools.update({ tooluid: deepcopy(tooluid_value) }) self.mtool_gen_cncjob() self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No tool selected in the tool table ...")) def mtool_gen_cncjob(self, outname=None, tools_dict=None, tools_in_use=None, segx=None, segy=None, plot=True, use_thread=True): """ Creates a multi-tool CNCJob out of this Geometry object. The actual work is done by the target CNCJobObject object's `generate_from_geometry_2()` method. :param outname: :param tools_dict: a dictionary that holds the whole data needed to create the Gcode (including the solid_geometry) :param tools_in_use: the tools that are used, needed by some preprocessors :type tools_in_use list of lists, each list in the list is made out of row elements of tools table from appGUI :param segx: number of segments on the X axis, for auto-levelling :param segy: number of segments on the Y axis, for auto-levelling :param plot: if True the generated object will be plotted; if False will not be plotted :param use_thread: if True use threading :return: None """ # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia outname = "%s_%s" % (self.options["name"], 'cnc') if outname is None else outname tools_dict = self.sel_tools if tools_dict is None else tools_dict tools_in_use = tools_in_use if tools_in_use is not None else self.get_selected_tools_table_items() 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']) try: xmin = self.options['xmin'] ymin = self.options['ymin'] xmax = self.options['xmax'] ymax = self.options['ymax'] except Exception as e: log.debug("FlatCAMObj.GeometryObject.mtool_gen_cncjob() --> %s\n" % str(e)) msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n") msg += '%s %s' % ('FlatCAMObj.GeometryObject.mtool_gen_cncjob() -->', str(e)) msg += traceback.format_exc() self.app.inform.emit(msg) return # Object initialization function for app.app_obj.new_object() # RUNNING ON SEPARATE THREAD! def job_init_single_geometry(job_obj, app_obj): log.debug("Creating a CNCJob out of a single-geometry") assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj) job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax # count the tools tool_cnt = 0 # dia_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True job_obj.multigeo = False job_obj.cnc_tools.clear() job_obj.options['Tools_in_use'] = tools_in_use job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"]) job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"]) job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"]) job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"]) for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 dia_cnc_dict = deepcopy(tools_dict[tooluid_key]) tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia']))) dia_cnc_dict.update({ 'tooldia': tooldia_val }) if dia_cnc_dict['offset'] == 'in': tool_offset = -dia_cnc_dict['tooldia'] / 2 elif dia_cnc_dict['offset'].lower() == 'out': tool_offset = dia_cnc_dict['tooldia'] / 2 elif dia_cnc_dict['offset'].lower() == 'custom': 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] %s' % _("Wrong value format entered, use a number.")) return if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit( '[WARNING] %s' % _("Tool Offset is selected in Tool Table but no value is provided.\n" "Add a Tool Offset or change the Offset Type.") ) return else: tool_offset = 0.0 dia_cnc_dict.update({ 'offset_value': tool_offset }) z_cut = tools_dict[tooluid_key]['data']["cutz"] z_move = tools_dict[tooluid_key]['data']["travelz"] feedrate = tools_dict[tooluid_key]['data']["feedrate"] feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"] feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"] multidepth = tools_dict[tooluid_key]['data']["multidepth"] extracut = tools_dict[tooluid_key]['data']["extracut"] extracut_length = tools_dict[tooluid_key]['data']["extracut_length"] depthpercut = tools_dict[tooluid_key]['data']["depthperpass"] toolchange = tools_dict[tooluid_key]['data']["toolchange"] toolchangez = tools_dict[tooluid_key]['data']["toolchangez"] toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"] startz = tools_dict[tooluid_key]['data']["startz"] endz = tools_dict[tooluid_key]['data']["endz"] endxy = self.options["endxy"] spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"] dwell = tools_dict[tooluid_key]['data']["dwell"] dwelltime = tools_dict[tooluid_key]['data']["dwelltime"] pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"] spindledir = self.app.defaults['geometry_spindledir'] tool_solid_geometry = self.solid_geometry job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options job_obj.options["tooldia"] = tooldia_val job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia_val # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 res = job_obj.generate_from_geometry_2( self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthpercut, extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) if res == 'fail': log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: dia_cnc_dict['gcode'] = res # 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_type = "geometry" self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("G-Code parsing finished...")) # TODO this serve for bounding box creation only; should be optimized # commented this; there is no need for the actual GCode geometry - the original one will serve as well # for bounding box values # dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']]) try: dia_cnc_dict['solid_geometry'] = tool_solid_geometry self.app.inform.emit('[success] %s...' % _("Finished G-Code processing")) except Exception as er: self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(er))) job_obj.cnc_tools.update({ tooluid_key: deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() # Object initialization function for app.app_obj.new_object() # RUNNING ON SEPARATE THREAD! def job_init_multi_geometry(job_obj, app_obj): log.debug("Creating a CNCJob out of a multi-geometry") assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj) job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax # count the tools tool_cnt = 0 # dia_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True job_obj.multigeo = True job_obj.cnc_tools.clear() job_obj.options['Tools_in_use'] = tools_in_use job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"]) job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"]) job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"]) job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"]) # make sure that trying to make a CNCJob from an empty file is not creating an app crash if not self.solid_geometry: a = 0 for tooluid_key in self.tools: if self.tools[tooluid_key]['solid_geometry'] is None: a += 1 if a == len(self.tools): self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry')) return 'fail' for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 dia_cnc_dict = deepcopy(tools_dict[tooluid_key]) tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia']))) dia_cnc_dict.update({ 'tooldia': tooldia_val }) # find the tool_dia associated with the tooluid_key # search in the self.tools for the sel_tool_dia and when found see what tooluid has # on the found tooluid in self.tools we also have the solid_geometry that interest us # for k, v in self.tools.items(): # if float('%.*f' % (self.decimals, float(v['tooldia']))) == tooldia_val: # current_uid = int(k) # break if dia_cnc_dict['offset'] == 'in': tool_offset = -tooldia_val / 2 elif dia_cnc_dict['offset'].lower() == 'out': tool_offset = tooldia_val / 2 elif dia_cnc_dict['offset'].lower() == 'custom': offset_value = float(self.ui.tool_offset_entry.get_value()) if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit('[WARNING] %s' % _("Tool Offset is selected in Tool Table but " "no value is provided.\n" "Add a Tool Offset or change the Offset Type.")) return else: tool_offset = 0.0 dia_cnc_dict.update({ 'offset_value': tool_offset }) z_cut = tools_dict[tooluid_key]['data']["cutz"] z_move = tools_dict[tooluid_key]['data']["travelz"] feedrate = tools_dict[tooluid_key]['data']["feedrate"] feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"] feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"] multidepth = tools_dict[tooluid_key]['data']["multidepth"] extracut = tools_dict[tooluid_key]['data']["extracut"] extracut_length = tools_dict[tooluid_key]['data']["extracut_length"] depthpercut = tools_dict[tooluid_key]['data']["depthperpass"] toolchange = tools_dict[tooluid_key]['data']["toolchange"] toolchangez = tools_dict[tooluid_key]['data']["toolchangez"] toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"] startz = tools_dict[tooluid_key]['data']["startz"] endz = tools_dict[tooluid_key]['data']["endz"] endxy = self.options["endxy"] spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"] dwell = tools_dict[tooluid_key]['data']["dwell"] dwelltime = tools_dict[tooluid_key]['data']["dwelltime"] pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"] spindledir = self.app.defaults['geometry_spindledir'] tool_solid_geometry = self.tools[tooluid_key]['solid_geometry'] job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options job_obj.options["tooldia"] = tooldia_val job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia_val # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 res = job_obj.generate_from_multitool_geometry( tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthpercut, extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) if res == 'fail': log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: dia_cnc_dict['gcode'] = res self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("G-Code parsing finished...")) # TODO this serve for bounding box creation only; should be optimized # commented this; there is no need for the actual GCode geometry - the original one will serve as well # for bounding box values # geo_for_bound_values = cascaded_union([ # geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True # ]) try: dia_cnc_dict['solid_geometry'] = deepcopy(tool_solid_geometry) self.app.inform.emit('[success] %s' % _("Finished G-Code processing...")) except Exception as ee: self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(ee))) # 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_type = "geometry" job_obj.cnc_tools.update({ tooluid_key: deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() if use_thread: # To be run in separate thread def job_thread(a_obj): if self.multigeo is False: with self.app.proc_container.new(_("Generating CNC Code")): if a_obj.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail': a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname)) else: with self.app.proc_container.new(_("Generating CNC Code")): if a_obj.app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail': a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname)) # Create a promise with the name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) else: if self.solid_geometry: self.app.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) else: self.app.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot) def generatecncjob(self, outname=None, dia=None, offset=None, z_cut=None, z_move=None, feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None, dwell=None, dwelltime=None, multidepth=None, dpp=None, toolchange=None, toolchangez=None, toolchangexy=None, extracut=None, extracut_length=None, startz=None, endz=None, endxy=None, pp=None, segx=None, segy=None, use_thread=True, plot=True): """ Only used by the TCL Command Cncjob. Creates a CNCJob out of this Geometry object. The actual work is done by the target camlib.CNCjob `generate_from_geometry_2()` method. :param outname: Name of the new object :param dia: Tool diameter :param offset: :param z_cut: Cut depth (negative value) :param z_move: Height of the tool when travelling (not cutting) :param feedrate: Feed rate while cutting on X - Y plane :param feedrate_z: Feed rate while cutting on Z plane :param feedrate_rapid: Feed rate while moving with rapids :param spindlespeed: Spindle speed (RPM) :param dwell: :param dwelltime: :param multidepth: :param dpp: Depth for each pass when multidepth parameter is True :param toolchange: :param toolchangez: :param toolchangexy: A sequence ox X,Y coordinates: a 2-length tuple or a string. Coordinates in X,Y plane for the Toolchange event :param extracut: :param extracut_length: :param startz: :param endz: :param endxy: A sequence ox X,Y coordinates: a 2-length tuple or a string. Coordinates in X, Y plane for the last move after ending the job. :param pp: Name of the preprocessor :param segx: :param segy: :param use_thread: :param plot: :return: None """ tooldia = dia if dia else float(self.options["cnctooldia"]) outname = outname if outname is not None else self.options["name"] z_cut = z_cut if z_cut is not None else float(self.options["cutz"]) z_move = z_move if z_move is not None else float(self.options["travelz"]) feedrate = feedrate if feedrate is not None else float(self.options["feedrate"]) feedrate_z = feedrate_z if feedrate_z is not None else float(self.options["feedrate_z"]) feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else float(self.options["feedrate_rapid"]) multidepth = multidepth if multidepth is not None else self.options["multidepth"] depthperpass = dpp if dpp is not None else float(self.options["depthperpass"]) 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']) extracut = extracut if extracut is not None else float(self.options["extracut"]) extracut_length = extracut_length if extracut_length is not None else float(self.options["extracut_length"]) startz = startz if startz is not None else self.options["startz"] endz = endz if endz is not None else float(self.options["endz"]) endxy = endxy if endxy else self.options["endxy"] if isinstance(endxy, str): endxy = re.sub('[()\[\]]', '', endxy) if endxy and endxy != '': endxy = [float(eval(a)) for a in endxy.split(",")] toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"]) toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"] if isinstance(toolchangexy, str): toolchangexy = re.sub('[()\[\]]', '', toolchangexy) if toolchangexy and toolchangexy != '': toolchangexy = [float(eval(a)) for a in toolchangexy.split(",")] toolchange = toolchange if toolchange else self.options["toolchange"] offset = offset if offset else 0.0 # int or None. spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed'] dwell = dwell if dwell else self.options["dwell"] dwelltime = dwelltime if dwelltime else float(self.options["dwelltime"]) ppname_g = pp if pp else self.options["ppname_g"] # Object initialization function for app.app_obj.new_object() # RUNNING ON SEPARATE THREAD! def job_init(job_obj, app_obj): assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj) # Propagate options job_obj.options["tooldia"] = tooldia job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia job_obj.segx = segx job_obj.segy = segy job_obj.z_pdepth = float(self.options["z_pdepth"]) job_obj.feedrate_probe = float(self.options["feedrate_probe"]) job_obj.options['xmin'] = self.options['xmin'] job_obj.options['ymin'] = self.options['ymin'] job_obj.options['xmax'] = self.options['xmax'] job_obj.options['ymax'] = self.options['ymax'] # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 job_obj.generate_from_geometry_2( self, tooldia=tooldia, offset=offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthperpass, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, pp_geometry_name=ppname_g ) # 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_type = "geometry" job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("Finished G-Code processing...")) if use_thread: # To be run in separate thread def job_thread(app_obj): with self.app.proc_container.new(_("Generating CNC Code")): app_obj.app_obj.new_object("cncjob", outname, job_init, plot=plot) app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname) # Create a promise with the name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) else: self.app.app_obj.new_object("cncjob", outname, job_init, plot=plot) # def on_plot_cb_click(self, *args): # if self.muted_ui: # return # self.read_form_item('plot') def scale(self, xfactor, yfactor=None, point=None): """ Scales all geometry by a given factor. :param xfactor: Factor by which to scale the object's geometry/ :type xfactor: float :param yfactor: Factor by which to scale the object's geometry/ :type yfactor: float :param point: Point around which to scale :return: None :rtype: None """ log.debug("FlatCAMObj.GeometryObject.scale()") try: xfactor = float(xfactor) except Exception: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float.")) return if yfactor is None: yfactor = xfactor else: try: yfactor = float(yfactor) except Exception: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float.")) return if xfactor == 1 and yfactor == 1: return if point is None: px = 0 py = 0 else: px, py = point self.geo_len = 0 self.old_disp_number = 0 self.el_count = 0 def scale_recursion(geom): if type(geom) is list: geoms = [] for local_geom in geom: geoms.append(scale_recursion(local_geom)) return geoms else: try: self.el_count += 1 disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) if self.old_disp_number < disp_number <= 100: self.app.proc_container.update_view_text(' %d%%' % disp_number) self.old_disp_number = disp_number return affinity.scale(geom, xfactor, yfactor, origin=(px, py)) except AttributeError: return geom if self.multigeo is True: for tool in self.tools: # variables to display the percentage of work done self.geo_len = 0 try: self.geo_len = len(self.tools[tool]['solid_geometry']) except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry']) try: # variables to display the percentage of work done self.geo_len = 0 try: self.geo_len = len(self.solid_geometry) except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.solid_geometry = scale_recursion(self.solid_geometry) except AttributeError: self.solid_geometry = [] return self.app.proc_container.new_text = '' self.app.inform.emit('[success] %s' % _("Geometry Scale done.")) def offset(self, vect): """ Offsets all geometry by a given vector/ :param vect: (x, y) vector by which to offset the object's geometry. :type vect: tuple :return: None :rtype: None """ log.debug("FlatCAMObj.GeometryObject.offset()") try: dx, dy = vect except TypeError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("An (x,y) pair of values are needed. " "Probable you entered only one value in the Offset field.") ) return if dx == 0 and dy == 0: return self.geo_len = 0 self.old_disp_number = 0 self.el_count = 0 def translate_recursion(geom): if type(geom) is list: geoms = [] for local_geom in geom: geoms.append(translate_recursion(local_geom)) return geoms else: try: self.el_count += 1 disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) if self.old_disp_number < disp_number <= 100: self.app.proc_container.update_view_text(' %d%%' % disp_number) self.old_disp_number = disp_number return affinity.translate(geom, xoff=dx, yoff=dy) except AttributeError: return geom if self.multigeo is True: for tool in self.tools: # variables to display the percentage of work done self.geo_len = 0 try: self.geo_len = len(self.tools[tool]['solid_geometry']) except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry']) # variables to display the percentage of work done self.geo_len = 0 try: self.geo_len = len(self.solid_geometry) except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.solid_geometry = translate_recursion(self.solid_geometry) self.app.proc_container.new_text = '' self.app.inform.emit('[success] %s' % _("Geometry Offset done.")) def convert_units(self, units): log.debug("FlatCAMObj.GeometryObject.convert_units()") self.ui_disconnect() factor = Geometry.convert_units(self, units) self.options['cutz'] = float(self.options['cutz']) * factor self.options['depthperpass'] = float(self.options['depthperpass']) * factor self.options['travelz'] = float(self.options['travelz']) * factor self.options['feedrate'] = float(self.options['feedrate']) * factor self.options['feedrate_z'] = float(self.options['feedrate_z']) * factor self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor self.options['endz'] = float(self.options['endz']) * factor # self.options['cnctooldia'] *= factor # self.options['painttooldia'] *= factor # self.options['paintmargin'] *= factor # self.options['paintoverlap'] *= factor self.options["toolchangez"] = float(self.options["toolchangez"]) * factor 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] %s' % _("The Toolchange X,Y field in Edit -> Preferences " "has to be in the format (x, y)\n" "but 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'] = float(self.options['startz']) * factor param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid', 'endz', 'toolchangez'] if isinstance(self, GeometryObject): temp_tools_dict = {} tool_dia_copy = {} data_copy = {} for tooluid_key, tooluid_value in self.tools.items(): for dia_key, dia_value in tooluid_value.items(): if dia_key == 'tooldia': dia_value *= factor dia_value = float('%.*f' % (self.decimals, 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 # convert the value in the Custom Tool Offset entry in UI custom_offset = None 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] %s' % _("Wrong value format entered, use a number.")) return except TypeError: pass if custom_offset: custom_offset *= factor self.ui.tool_offset_entry.set_value(custom_offset) 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] = deepcopy(data_copy) data_copy.clear() temp_tools_dict.update({ tooluid_key: deepcopy(tool_dia_copy) }) tool_dia_copy.clear() self.tools.clear() self.tools = deepcopy(temp_tools_dict) # if there is a value in the new tool field then convert that one too try: self.ui.addtool_entry.returnPressed.disconnect() except TypeError: pass tooldia = self.ui.addtool_entry.get_value() if tooldia: tooldia *= factor tooldia = float('%.*f' % (self.decimals, tooldia)) self.ui.addtool_entry.set_value(tooldia) self.ui.addtool_entry.returnPressed.connect(self.on_tool_add) return factor def on_add_area_click(self): shape_button = self.ui.area_shape_radio overz_button = self.ui.over_z_entry strategy_radio = self.ui.strategy_radio cnc_button = self.ui.generate_cnc_button solid_geo = self.solid_geometry obj_type = self.kind self.app.exc_areas.on_add_area_click( shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio, solid_geo=solid_geo, obj_type=obj_type) def on_clear_area_click(self): if not self.app.exc_areas.exclusion_areas_storage: self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. There are no exclusion areas to delete.")) return self.app.exc_areas.on_clear_area_click() self.app.exc_areas.e_shape_modified.emit() def on_delete_sel_areas(self): sel_model = self.ui.exclusion_table.selectionModel() sel_indexes = sel_model.selectedIndexes() # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows # so the duplicate rows will not be added sel_rows = set() for idx in sel_indexes: sel_rows.add(idx.row()) if not sel_rows: self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. Nothing is selected.")) return self.app.exc_areas.delete_sel_shapes(idxs=list(sel_rows)) self.app.exc_areas.e_shape_modified.emit() def draw_sel_shape(self): sel_model = self.ui.exclusion_table.selectionModel() sel_indexes = sel_model.selectedIndexes() # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows sel_rows = set() for idx in sel_indexes: sel_rows.add(idx.row()) self.delete_sel_shape() if self.app.is_legacy is False: face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.2 * 255)))[2:] outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(0.8 * 255)))[2:] else: face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.4 * 255)))[2:] outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(1.0 * 255)))[2:] for row in sel_rows: sel_rect = self.app.exc_areas.exclusion_areas_storage[row]['shape'] self.app.move_tool.sel_shapes.add(sel_rect, color=outline, face_color=face, update=True, layer=0, tolerance=None) if self.app.is_legacy is True: self.app.move_tool.sel_shapes.redraw() def clear_selection(self): self.app.delete_selection_shape() # self.ui.exclusion_table.clearSelection() def delete_sel_shape(self): self.app.delete_selection_shape() def update_exclusion_table(self): self.exclusion_area_cb_is_checked = True if self.ui.exclusion_cb.isChecked() else False self.build_ui() self.ui.exclusion_cb.set_value(self.exclusion_area_cb_is_checked) def on_strategy(self, val): if val == 'around': self.ui.over_z_label.setDisabled(True) self.ui.over_z_entry.setDisabled(True) else: self.ui.over_z_label.setDisabled(False) self.ui.over_z_entry.setDisabled(False) def exclusion_table_toggle_all(self): """ will toggle the selection of all rows in Exclusion Areas table :return: """ sel_model = self.ui.exclusion_table.selectionModel() sel_indexes = sel_model.selectedIndexes() # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows sel_rows = set() for idx in sel_indexes: sel_rows.add(idx.row()) if sel_rows: self.ui.exclusion_table.clearSelection() self.delete_sel_shape() else: self.ui.exclusion_table.selectAll() self.draw_sel_shape() def plot_element(self, element, color=None, visible=None): if color is None: color = '#FF0000FF' visible = visible if visible else self.options['plot'] try: for sub_el in element: self.plot_element(sub_el, color=color) except TypeError: # Element is not iterable... # if self.app.is_legacy is False: self.add_shape(shape=element, color=color, visible=visible, layer=0) def plot(self, visible=None, kind=None): """ Plot the object. :param visible: Controls if the added shape is visible of not :param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob, because CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot() has to be rewrited :return: """ # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return if self.app.is_legacy is False: def random_color(): r_color = np.random.rand(4) r_color[3] = 1 return r_color else: def random_color(): while True: r_color = np.random.rand(4) r_color[3] = 1 new_color = '#' for idx in range(len(r_color)): new_color += '%x' % int(r_color[idx] * 255) # do it until a valid color is generated # a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha # for a total of 9 chars if len(new_color) == 9: break return new_color try: # plot solid geometries found as members of self.tools attribute dict # for MultiGeo if self.multigeo is True: # geo multi tool usage for tooluid_key in self.tools: solid_geometry = self.tools[tooluid_key]['solid_geometry'] self.plot_element(solid_geometry, visible=visible, color=random_color() if self.options['multicolored'] else self.app.defaults["geometry_plot_line"]) else: # plot solid geometry that may be an direct attribute of the geometry object # for SingleGeo if self.solid_geometry: self.plot_element(self.solid_geometry, visible=visible, color=self.app.defaults["geometry_plot_line"]) # self.plot_element(self.solid_geometry, visible=self.options['plot']) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) def on_plot_cb_click(self, *args): if self.muted_ui: return self.read_form_item('plot') self.plot() self.ui_disconnect() cb_flag = self.ui.plot_cb.isChecked() for row in range(self.ui.geo_tools_table.rowCount()): table_cb = self.ui.geo_tools_table.cellWidget(row, 6) if cb_flag: table_cb.setChecked(True) else: table_cb.setChecked(False) self.ui_connect() def on_plot_cb_click_table(self): # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked) self.ui_disconnect() # cw = self.sender() # cw_index = self.ui.geo_tools_table.indexAt(cw.pos()) # cw_row = cw_index.row() check_row = 0 self.shapes.clear(update=True) for tooluid_key in self.tools: solid_geometry = self.tools[tooluid_key]['solid_geometry'] # find the geo_tool_table row associated with the tooluid_key for row in range(self.ui.geo_tools_table.rowCount()): tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text()) if tooluid_item == int(tooluid_key): check_row = row break if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked(): self.plot_element(element=solid_geometry, visible=True) self.shapes.redraw() # make sure that the general plot is disabled if one of the row plot's are disabled and # if all the row plot's are enabled also enable the general plot checkbox cb_cnt = 0 total_row = self.ui.geo_tools_table.rowCount() for row in range(total_row): if self.ui.geo_tools_table.cellWidget(row, 6).isChecked(): cb_cnt += 1 else: cb_cnt -= 1 if cb_cnt < total_row: self.ui.plot_cb.setChecked(False) else: self.ui.plot_cb.setChecked(True) self.ui_connect() def on_multicolored_cb_click(self, *args): if self.muted_ui: return self.read_form_item('multicolored') self.plot() @staticmethod def merge(geo_list, geo_final, multigeo=None): """ Merges the geometry of objects in grb_list into the geometry of geo_final. :param geo_list: List of GerberObject Objects to join. :param geo_final: Destination GerberObject object. :param multigeo: if the merged geometry objects are of type MultiGeo :return: None """ if geo_final.solid_geometry is None: geo_final.solid_geometry = [] try: __ = iter(geo_final.solid_geometry) except TypeError: geo_final.solid_geometry = [geo_final.solid_geometry] new_solid_geometry = [] new_options = {} new_tools = {} for geo_obj in geo_list: for option in geo_obj.options: if option != 'name': try: new_options[option] = deepcopy(geo_obj.options[option]) except Exception as e: log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e))) # Expand lists if type(geo_obj) is list: GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final) # If not list, just append else: if multigeo is None or multigeo is False: geo_final.multigeo = False else: geo_final.multigeo = True try: new_solid_geometry += deepcopy(geo_obj.solid_geometry) except Exception as e: log.debug("GeometryObject.merge() --> %s" % str(e)) # find the tool_uid maximum value in the geo_final try: max_uid = max([int(i) for i in new_tools.keys()]) except ValueError: max_uid = 0 # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try # to merge the obj.tools as it is likely there is none to merge. if geo_obj.kind != 'gerber' and geo_obj.kind != 'excellon': for tool_uid in geo_obj.tools: max_uid += 1 new_tools[max_uid] = deepcopy(geo_obj.tools[tool_uid]) geo_final.options.update(new_options) geo_final.solid_geometry = new_solid_geometry geo_final.tools = new_tools @staticmethod def get_pts(o): """ Returns a list of all points in the object, where the object can be a MultiPolygon, Polygon, Not a polygon, or a list of such. Search is done recursively. :param: geometric object :return: List of points :rtype: list """ pts = [] # Iterable: descend into each item. try: for subo in o: pts += GeometryObject.get_pts(subo) # Non-iterable except TypeError: if o is not None: if type(o) == MultiPolygon: for poly in o: pts += GeometryObject.get_pts(poly) # ## Descend into .exerior and .interiors elif type(o) == Polygon: pts += GeometryObject.get_pts(o.exterior) for i in o.interiors: pts += GeometryObject.get_pts(i) elif type(o) == MultiLineString: for line in o: pts += GeometryObject.get_pts(line) # ## Has .coords: list them. else: pts += list(o.coords) else: return return pts