# ########################################################## # 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 PyQt6 import QtWidgets, QtCore, QtGui from appParsers.ParseExcellon import Excellon from appObjects.AppObjectTemplate import FlatCAMObj, ObjectDeleted from appGUI.GUIElements import FCCheckBox from appGUI.ObjectUI import ExcellonObjectUI import itertools import numpy as np from copy import deepcopy from shapely import LineString import gettext import appTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext class ExcellonObject(FlatCAMObj, Excellon): """ Represents Excellon/Drill code. An object stored in the FlatCAM objects collection (a dict) """ ui_type = ExcellonObjectUI optionChanged = QtCore.pyqtSignal(str) multicolored_build_sig = QtCore.pyqtSignal() def __init__(self, name): self.decimals = self.app.decimals self.circle_steps = int(self.app.options["excellon_circle_steps"]) Excellon.__init__(self, excellon_circle_steps=self.circle_steps) FlatCAMObj.__init__(self, name) self.kind = "excellon" self.obj_options.update({ "plot": True, "solid": False, "multicolored": False, "merge_fuse_tools": True, "drill_tooldia": 0.1, "slot_tooldia": 0.1, "format_upper_in": 2, "format_lower_in": 4, "format_upper_mm": 3, "lower_mm": 3, "zeros": "T", "units": "INCH", "update": True, "optimization_type": "B", "search_time": 3 }) # TODO: Document this. self.tool_cbs = {} # dict that holds the object names and the option name # the key is the object name (defines in ObjectUI) for each UI element that is a parameter # particular for a tool and the value is the actual name of the option that the UI element is changing self.name2option = {} # default set of data to be added to each tool in self.tools as self.tools[tool]['data'] = self.default_data self.default_data = {} # variable to store the total amount of drills per job self.tot_drill_cnt = 0 self.tool_row = 0 # variable to store the total amount of slots per job self.tot_slot_cnt = 0 self.tool_row_slots = 0 # variable to store the distance travelled self.travel_distance = 0.0 # store the source file here self.source_file = "" self.multigeo = False self.units_found = self.app.app_units self.fill_color = self.app.options['excellon_plot_fill'] self.outline_color = self.app.options['excellon_plot_line'] self.alpha_level = 'bf' # the key is the tool id and the value is a list of shapes keys (indexes) self.shape_indexes_dict = {} # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs = ['obj_options', 'kind', 'fill_color', 'outline_color', 'alpha_level'] + self.ser_attrs def set_ui(self, ui): """ Configures the user interface for this object. Connects options to form fields. :param ui: User interface object. :type ui: ExcellonObjectUI :return: None """ FlatCAMObj.set_ui(self, ui) self.app.log.debug("ExcellonObject.set_ui()") self.units = self.app.app_units.upper() # # fill in self.obj_options values for the Drilling Tool from self.app.options # for opt_key, opt_val in self.app.options.items(): # if opt_key.find('tools_drill_') == 0: # self.obj_options[opt_key] = deepcopy(opt_val) # # # fill in self.default_data values from self.obj_options # for opt_key, opt_val in self.app.options.items(): # if opt_key.find('excellon_') == 0 or opt_key.find('tools_drill_') == 0: # self.default_data[opt_key] = deepcopy(opt_val) self.form_fields.update({ "plot": self.ui.plot_cb, "solid": self.ui.solid_cb, "multicolored": self.ui.multicolored_cb, "autoload_db": self.ui.autoload_db_cb, "drill_tooldia": self.ui.tooldia_entry, "slot_tooldia": self.ui.slot_tooldia_entry, }) self.to_form() assert isinstance(self.ui, ExcellonObjectUI), \ "Expected a ExcellonObjectUI, got %s" % type(self.ui) # ############################################################################# # ############################ SIGNALS ######################################## # ############################################################################# self.ui.level.toggled.connect(self.on_level_changed) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click) self.multicolored_build_sig.connect(self.on_multicolored_build) self.ui.autoload_db_cb.stateChanged.connect(self.on_autoload_db_toggled) # Editor self.ui.editor_button.clicked.connect(lambda: self.app.on_editing_start()) # Properties self.ui.info_button.toggled.connect(self.on_properties) self.calculations_finished.connect(self.update_area_chull) self.ui.treeWidget.itemExpanded.connect(self.on_properties_expanded) self.ui.treeWidget.itemCollapsed.connect(self.on_properties_expanded) self.ui.drill_button.clicked.connect(lambda: self.app.drilling_tool.run(toggle=True)) self.ui.milling_button.clicked.connect(self.on_milling_button_clicked) # UTILITIES self.ui.util_button.clicked.connect(lambda st: self.ui.util_frame.show() if st else self.ui.util_frame.hide()) self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click) self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click) # Toggle all Table rows self.ui.tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_rows) self.ui.table_visibility_cb.stateChanged.connect(self.on_table_visibility_toggle) self.units_found = self.app.app_units self.set_offset_values() self.clear_contex_menu() self.init_context_menu() # Show/Hide Advanced Options app_mode = self.app.options["global_app_level"] self.change_level(app_mode) def set_offset_values(self): xmin, ymin, xmax, ymax = self.bounds() center_coords = ( self.app.dec_format((xmin + abs((xmax - xmin) / 2)), self.decimals), self.app.dec_format((ymin + abs((ymax - ymin) / 2)), self.decimals) ) self.ui.offsetvector_entry.set_value(str(center_coords)) def change_level(self, level): """ :param level: application level: either 'b' or 'a' :type level: str :return: """ if level == 'a': self.ui.level.setChecked(True) else: self.ui.level.setChecked(False) self.on_level_changed(self.ui.level.isChecked()) def on_level_changed(self, checked): if not checked: self.ui.level.setText('%s' % _('Beginner')) self.ui.level.setStyleSheet(""" QToolButton { color: green; } """) self.ui.tools_table.setColumnHidden(4, True) self.ui.tools_table.setColumnHidden(5, True) self.ui.table_visibility_cb.set_value(True) self.ui.table_visibility_cb.hide() self.ui.autoload_db_cb.hide() # Context Menu section self.ui.tools_table.removeContextMenu() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.level.setStyleSheet(""" QToolButton { color: red; } """) self.ui.tools_table.setColumnHidden(4, False) self.ui.tools_table.setColumnHidden(5, False) self.ui.table_visibility_cb.show() self.ui.table_visibility_cb.set_value(self.app.options["excellon_tools_table_display"]) self.on_table_visibility_toggle(state=self.app.options["excellon_tools_table_display"]) self.ui.autoload_db_cb.show() # Context Menu section self.ui.tools_table.setupContextMenu() def build_ui(self): """ Will (re)build the Excellon UI updating it (the tool table) :return: None :rtype: """ FlatCAMObj.build_ui(self) self.units = self.app.app_units.upper() for row in range(self.ui.tools_table.rowCount()): try: # if connected, disconnect the signal from the slot on item_changed as it creates issues offset_spin_widget = self.ui.tools_table.cellWidget(row, 4) offset_spin_widget.valueChanged.disconnect() except (TypeError, AttributeError): pass n = len(self.tools) # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals. self.ui.tools_table.setRowCount(n + 2) self.tot_drill_cnt = 0 self.tot_slot_cnt = 0 self.tool_row = 0 sort = [] for k, v in list(self.tools.items()): try: sort.append((k, v['tooldia'])) except KeyError: # for old projects to be opened sort.append((k, v['C'])) sorted_tools = sorted(sort, key=lambda t1: t1[1]) tools = [i[0] for i in sorted_tools] new_options = {} for opt in self.obj_options: new_options[opt] = self.obj_options[opt] for tool_no in tools: try: dia_val = self.tools[tool_no]['tooldia'] except KeyError: # for old projects to be opened dia_val = self.tools[tool_no]['C'] # add the data dictionary for each tool with the default values self.tools[tool_no]['data'] = deepcopy(new_options) # drill_cnt = 0 # variable to store the nr of drills per tool # slot_cnt = 0 # variable to store the nr of slots per tool # Find no of drills for the current tool try: drill_cnt = len(self.tools[tool_no]['drills']) except KeyError: drill_cnt = 0 self.tot_drill_cnt += drill_cnt # Find no of slots for the current tool try: slot_cnt = len(self.tools[tool_no]['slots']) except KeyError: slot_cnt = 0 self.tot_slot_cnt += slot_cnt # Tool ID exc_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_no)) exc_id_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, exc_id_item) # Tool name/id # Diameter dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, dia_val)) dia_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 1, dia_item) # Diameter # Drill count drill_count_item = QtWidgets.QTableWidgetItem('%d' % drill_cnt) drill_count_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 2, drill_count_item) # Number of drills per tool # Slot Count # if the slot number is zero is better to not clutter the GUI with zero's so we print a space slot_count_str = '%d' % slot_cnt if slot_cnt > 0 else '' slot_count_item = QtWidgets.QTableWidgetItem(slot_count_str) slot_count_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 3, slot_count_item) # Number of drills per tool # Empty Plot Item empty_plot_item = QtWidgets.QTableWidgetItem('') empty_plot_item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) self.ui.tools_table.setItem(self.tool_row, 4, empty_plot_item) if 'multicolor' in self.tools[tool_no] and self.tools[tool_no]['multicolor'] is not None: red = int(self.tools[tool_no]['multicolor'][0] * 255) green = int(self.tools[tool_no]['multicolor'][1] * 255) blue = int(self.tools[tool_no]['multicolor'][2] * 255) alpha = int(self.tools[tool_no]['multicolor'][3] * 255) h_color = QtGui.QColor(red, green, blue, alpha) self.ui.tools_table.item(self.tool_row, 4).setBackground(h_color) else: h1 = self.app.options["excellon_plot_fill"][1:7] h2 = self.app.options["excellon_plot_fill"][7:9] h_color = QtGui.QColor('#' + h2 + h1) self.ui.tools_table.item(self.tool_row, 4).setBackground(h_color) # Plot Item plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) self.ui.tools_table.setCellWidget(self.tool_row, 5, plot_item) self.tool_row += 1 # add a last row with the Total number of drills empty_1 = QtWidgets.QTableWidgetItem('') empty_1.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_1_1 = QtWidgets.QTableWidgetItem('') empty_1_1.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_1_2 = QtWidgets.QTableWidgetItem('') empty_1_2.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_1_3 = QtWidgets.QTableWidgetItem('') empty_1_3.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_1_4 = QtWidgets.QTableWidgetItem('') empty_1_4.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills')) tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt) label_tot_drill_count.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) tot_drill_count.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, empty_1) self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count) self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count) # Total number of drills self.ui.tools_table.setItem(self.tool_row, 3, empty_1_1) self.ui.tools_table.setItem(self.tool_row, 4, empty_1_2) self.ui.tools_table.setItem(self.tool_row, 5, empty_1_3) font = QtGui.QFont() font.setBold(True) # font.setWeight(75) for k in [1, 2]: self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255)) self.ui.tools_table.item(self.tool_row, k).setFont(font) self.tool_row += 1 # add a last row with the Total number of slots empty_2 = QtWidgets.QTableWidgetItem('') empty_2.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_2_1 = QtWidgets.QTableWidgetItem('') empty_2_1.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_2_2 = QtWidgets.QTableWidgetItem('') empty_2_2.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_2_3 = QtWidgets.QTableWidgetItem('') empty_2_3.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) empty_2_4 = QtWidgets.QTableWidgetItem('') empty_2_4.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots')) tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt) label_tot_slot_count.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) tot_slot_count.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, empty_2) self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count) self.ui.tools_table.setItem(self.tool_row, 2, empty_2_1) self.ui.tools_table.setItem(self.tool_row, 3, tot_slot_count) # Total number of slots self.ui.tools_table.setItem(self.tool_row, 4, empty_2_3) self.ui.tools_table.setItem(self.tool_row, 5, empty_2_4) for kl in [1, 2, 3]: self.ui.tools_table.item(self.tool_row, kl).setFont(font) self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255)) # sort the tool diameter column # self.ui.tools_table.sortItems(1) # all the tools are selected by default self.ui.tools_table.selectColumn(0) self.ui.tools_table.resizeColumnsToContents() self.ui.tools_table.resizeRowsToContents() vertical_header = self.ui.tools_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) horizontal_header = self.ui.tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeMode.Fixed) horizontal_header.resizeSection(4, 17) horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeMode.Fixed) horizontal_header.resizeSection(5, 17) self.ui.tools_table.setColumnWidth(5, 17) # horizontal_header.setStretchLastSection(True) # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents) # horizontal_header.setStretchLastSection(True) self.ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.ui.tools_table.setSortingEnabled(False) self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight()) self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight()) # find if we have drills: has_drills = None for tt in self.tools: if 'drills' in self.tools[tt] and self.tools[tt]['drills']: has_drills = True break if has_drills is None: self.ui.tooldia_entry.setDisabled(True) self.ui.generate_milling_button.setDisabled(True) else: self.ui.tooldia_entry.setDisabled(False) self.ui.generate_milling_button.setDisabled(False) # find if we have slots has_slots = None for tt in self.tools: if 'slots' in self.tools[tt] and self.tools[tt]['slots']: has_slots = True break if has_slots is None: self.ui.slot_tooldia_entry.setDisabled(True) self.ui.generate_milling_slots_button.setDisabled(True) else: self.ui.slot_tooldia_entry.setDisabled(False) self.ui.generate_milling_slots_button.setDisabled(False) # update the milling section self.on_row_selection_change() self.ui_connect() def clear_contex_menu(self): self.ui.tools_table.removeContextMenu() def init_context_menu(self): # ############################################################################# # ###################### Setup CONTEXT MENU ################################### # ############################################################################# self.ui.tools_table.setupContextMenu() self.ui.tools_table.addContextMenu( _("Copy"), lambda: self.on_table_copy_dia(), icon=QtGui.QIcon(self.app.resource_location + "/copy32.png") ) def on_table_copy_dia(self): sel_model = self.ui.tools_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()) sel_rows_list = list(sel_rows) if len(sel_rows_list) == 1: copied_dia_text = self.ui.tools_table.item(sel_rows_list[0], 1).text() try: # only rows that have float values are allowed copied_dia = float(copied_dia_text) except ValueError: return else: copied_dia = self.ui.tools_table.item(sel_rows_list[0], 1).text() for row_idx in sel_rows_list[1:]: copied_dia_text = self.ui.tools_table.item(row_idx, 1).text() try: # this is done to avoid selected rows that do not have real numbers in the tool diameter column copied_test_dia = float(copied_dia_text) copied_dia += ',' copied_dia += str(copied_test_dia) except ValueError: continue self.app.clipboard.setText(str(copied_dia)) self.app.inform.emit('[success] %s' % _("Copied to clipboard.")) def ui_connect(self): """ Will connect all signals in the Excellon UI that needs to be connected :return: None :rtype: """ # selective plotting for row in range(self.ui.tools_table.rowCount() - 2): self.ui.tools_table.cellWidget(row, 5).clicked.connect(self.on_plot_cb_click_table) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) # rows selected self.ui.tools_table.clicked.connect(self.on_row_selection_change) def ui_disconnect(self): """ Will disconnect all signals in the Excellon UI that needs to be disconnected :return: None :rtype: """ # selective plotting for row in range(self.ui.tools_table.rowCount()): try: self.ui.tools_table.cellWidget(row, 5).clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.plot_cb.stateChanged.disconnect() except (TypeError, AttributeError): pass # rows selected try: self.ui.tools_table.clicked.disconnect() except (TypeError, AttributeError): pass def on_row_selection_change(self): """ Called when the user clicks on a row in Tools Table :return: None :rtype: """ self.ui_disconnect() sel_model = self.ui.tools_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 not sel_rows: self.ui.tooldia_entry.setDisabled(True) self.ui.generate_milling_button.setDisabled(True) self.ui.slot_tooldia_entry.setDisabled(True) self.ui.generate_milling_slots_button.setDisabled(True) self.ui_connect() return else: self.ui.tooldia_entry.setDisabled(False) self.ui.generate_milling_button.setDisabled(False) self.ui.slot_tooldia_entry.setDisabled(False) self.ui.generate_milling_slots_button.setDisabled(False) has_drills = None has_slots = None for row in sel_rows: row_dia = self.app.dec_format(float(self.ui.tools_table.item(row, 1).text()), self.decimals) for tt in self.tools: tool_dia = self.app.dec_format(float(self.tools[tt]['tooldia']), self.decimals) if tool_dia == row_dia: # find if we have drills: if 'drills' in self.tools[tt] and self.tools[tt]['drills']: has_drills = True # find if we have slots if 'slots' in self.tools[tt] and self.tools[tt]['slots']: has_slots = True if has_drills is None: self.ui.tooldia_entry.setDisabled(True) self.ui.generate_milling_button.setDisabled(True) else: self.ui.tooldia_entry.setDisabled(False) self.ui.generate_milling_button.setDisabled(False) if has_slots is None: self.ui.slot_tooldia_entry.setDisabled(True) self.ui.generate_milling_slots_button.setDisabled(True) else: self.ui.slot_tooldia_entry.setDisabled(False) self.ui.generate_milling_slots_button.setDisabled(False) self.ui_connect() def on_toggle_rows(self): sel_model = self.ui.tools_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()) # subtract the last 2 rows that show the total and are always displayed but not selected if len(sel_rows) == self.ui.tools_table.rowCount() - 2: self.ui.tools_table.clearSelection() else: self.ui.tools_table.selectAll() self.on_row_selection_change() def get_selected_tools_list(self): """ Returns the keys to the self.tools dictionary corresponding to the selections on the tool list in the appGUI. :return: List of tools. :rtype: list """ rows = set() for item in self.ui.tools_table.selectedItems(): rows.add(item.row()) tool_ids = [] for row in rows: tool_ids.append(int(self.ui.tools_table.item(row, 0).text())) return tool_ids # return [x.text() for x in self.ui.tools_table.selectedItems()] 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 = [] for x in self.ui.tools_table.selectedItems(): # from the columnCount we subtract a value of 1 which represent the last column (plot column) # which does not have text txt = '' elem = [] for column in range(0, self.ui.tools_table.columnCount() - 1): try: txt = self.ui.tools_table.item(x.row(), column).text() except AttributeError: try: txt = self.ui.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.tools_table.item(x.row(), column).text() # for column in range(0, self.ui.tools_table.columnCount() - 1)]) for item in table_tools_items: item[0] = str(item[0]) return table_tools_items def on_table_visibility_toggle(self, state): self.ui.tools_table.show() if state else self.ui.tools_table.hide() def on_properties(self, state): if state: self.ui.info_frame.show() else: self.ui.info_frame.hide() return self.ui.treeWidget.clear() self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget) self.ui.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.MinimumExpanding) # make sure that the FCTree widget columns are resized to content self.ui.treeWidget.resize_sig.emit() def on_properties_expanded(self): for col in range(self.treeWidget.columnCount()): self.ui.treeWidget.resizeColumnToContents(col) def on_milling_button_clicked(self): self.app.milling_tool.run(toggle=True) def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'): """ Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code :param whole: Integer part digits :type whole: int :param fract: Fractional part digits :type fract: int :param e_zeros: Excellon zeros suppression: LZ or TZ :type e_zeros: str :param form: Excellon format: 'dec', :type form: str :param factor: Conversion factor :type factor: float :param slot_type: How to treat slots: "routing" or "drilling" :type slot_type: str :return: A tuple: (has_slots, Excellon_code) -> (bool, str) :rtype: tuple """ excellon_code = '' # store here if the file has slots, return 1 if any slots, 0 if only drills slots_in_file = 0 # find if we have drills: has_drills = None for tt in self.tools: if 'drills' in self.tools[tt] and self.tools[tt]['drills']: has_drills = True break # find if we have slots: has_slots = None for tt in self.tools: if 'slots' in self.tools[tt] and self.tools[tt]['slots']: has_slots = True slots_in_file = 1 break # drills processing if has_drills: length = whole + fract for tool in self.tools: excellon_code += 'T0%s\n' % str(tool) if int(tool) < 10 else 'T%s\n' % str(tool) for drill in self.tools[tool]['drills']: if form == 'dec': try: drill_x = drill.x * factor drill_y = drill.y * factor excellon_code += "X{:.{dec}f}Y{:.{dec}f}\n".format(drill_x, drill_y, dec=fract) except Exception as e: self.app.log.error('ExcellonObject.export_excellon() drills "dec" -> %s' % str(e)) elif e_zeros == 'LZ': try: drill_x = drill.x * factor drill_y = drill.y * factor exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract) exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract) # extract whole part and decimal part exc_x_formatted = exc_x_formatted.partition('.') exc_y_formatted = exc_y_formatted.partition('.') # left pad the 'whole' part with zeros x_whole = exc_x_formatted[0].rjust(whole, '0') y_whole = exc_y_formatted[0].rjust(whole, '0') # restore the coordinate padded in the left with 0 and added the decimal part # without the decinal dot exc_x_formatted = x_whole + exc_x_formatted[2] exc_y_formatted = y_whole + exc_y_formatted[2] excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted, yform=exc_y_formatted) except Exception as e: self.app.log.error('ExcellonObject.export_excellon() drills "LZ" -> %s' % str(e)) else: try: drill_x = drill.x * factor drill_y = drill.y * factor exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract).replace('.', '') exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract).replace('.', '') # pad with rear zeros exc_x_formatted.ljust(length, '0') exc_y_formatted.ljust(length, '0') excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted, yform=exc_y_formatted) except Exception as e: self.app.log.error('ExcellonObject.export_excellon() drills "TZ" -> %s' % str(e)) # slots processing if has_slots: for tool in self.tools: excellon_code += 'G05\n' if int(tool) < 10: excellon_code += 'T0' + str(tool) + '\n' else: excellon_code += 'T' + str(tool) + '\n' for slot in self.tools[tool]['slots']: if form == 'dec': try: start_slot_x = slot[0].x * factor start_slot_y = slot[0].y * factor stop_slot_x = slot[1].x * factor stop_slot_y = slot[1].y * factor if slot_type == 'routing': excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM15\n".format(start_slot_x, start_slot_y, dec=fract) excellon_code += "G01X{:.{dec}f}Y{:.{dec}f}\nM16\n".format(stop_slot_x, stop_slot_y, dec=fract) elif slot_type == 'drilling': excellon_code += "X{:.{dec}f}Y{:.{dec}f}G85X{:.{dec}f}Y{:.{dec}f}\nG05\n".format( start_slot_x, start_slot_y, stop_slot_x, stop_slot_y, dec=fract ) except Exception as err: self.app.log.error('ExcellonObject.export_excellon() slots "dec" -> %s' % str(err)) elif e_zeros == 'LZ': try: start_slot_x = slot[0].x * factor start_slot_y = slot[0].y * factor stop_slot_x = slot[1].x * factor stop_slot_y = slot[1].y * factor start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '') start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '') stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '') stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '') # extract whole part and decimal part start_slot_x_formatted = start_slot_x_formatted.partition('.') start_slot_y_formatted = start_slot_y_formatted.partition('.') stop_slot_x_formatted = stop_slot_x_formatted.partition('.') stop_slot_y_formatted = stop_slot_y_formatted.partition('.') # left pad the 'whole' part with zeros start_x_whole = start_slot_x_formatted[0].rjust(whole, '0') start_y_whole = start_slot_y_formatted[0].rjust(whole, '0') stop_x_whole = stop_slot_x_formatted[0].rjust(whole, '0') stop_y_whole = stop_slot_y_formatted[0].rjust(whole, '0') # restore the coordinate padded in the left with 0 and added the decimal part # without the decinal dot start_slot_x_formatted = start_x_whole + start_slot_x_formatted[2] start_slot_y_formatted = start_y_whole + start_slot_y_formatted[2] stop_slot_x_formatted = stop_x_whole + stop_slot_x_formatted[2] stop_slot_y_formatted = stop_y_whole + stop_slot_y_formatted[2] if slot_type == 'routing': excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted, ystart=start_slot_y_formatted) excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted) elif slot_type == 'drilling': excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format( xstart=start_slot_x_formatted, ystart=start_slot_y_formatted, xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted ) except Exception as err: self.app.log.error('ExcellonObject.export_excellon() slots "LZ" -> %s' % str(err)) else: try: start_slot_x = slot[0].x * factor start_slot_y = slot[0].y * factor stop_slot_x = slot[1].x * factor stop_slot_y = slot[1].y * factor length = whole + fract start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '') start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '') stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '') stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '') # pad with rear zeros start_slot_x_formatted.ljust(length, '0') start_slot_y_formatted.ljust(length, '0') stop_slot_x_formatted.ljust(length, '0') stop_slot_y_formatted.ljust(length, '0') if slot_type == 'routing': excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted, ystart=start_slot_y_formatted) excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted) elif slot_type == 'drilling': excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format( xstart=start_slot_x_formatted, ystart=start_slot_y_formatted, xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted ) except Exception as err: self.app.log.error('ExcellonObject.export_excellon() slots "TZ" -> %s' % str(err)) if not has_drills and not has_slots: self.app.log.debug("ExcellonObject.export_excellon() --> Excellon Object is empty: no drills, no slots.") return 'fail' return slots_in_file, excellon_code def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False): """ Will generate an Geometry Object allowing to cut a drill hole instead of drilling it. Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. :param tools: A list of tools where the drills are to be milled or a string: "all" :type tools: :param outname: the name of the resulting Geometry object :type outname: str :param tooldia: the tool diameter to be used in creation of the milling path (Geometry Object) :type tooldia: float :param plot: if to plot the resulting object :type plot: bool :param use_thread: if to use threading for creation of the Geometry object :type use_thread: bool :return: Success/failure condition tuple (bool, str). :rtype: tuple """ # Get the tools from the list. These are keys # to self.tools if tools is None: tools = self.get_selected_tools_list() if outname is None: outname = self.obj_options["name"] + "_mill" if tooldia is None: tooldia = self.ui.tooldia_entry.get_value() # Sort tools by diameter. items() -> [('name', diameter), ...] # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3 sort = [] for k, v in self.tools.items(): sort.append((k, v['tooldia'])) sorted_tools = sorted(sort, key=lambda t1: t1[1]) if tools == "all": tools = [i[0] for i in sorted_tools] # List if ordered tool names. self.app.log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")) return False, "Error: No tools." for tool in tools: if tooldia > self.tools[tool]["tooldia"]: mseg = '[ERROR_NOTCL] %s %s: %s' % (_("Milling tool for DRILLS is larger than hole size. Cancelled."), _("Tool"), str(tool)) self.app.inform.emit(mseg) return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): """ :param geo_obj: New object :type geo_obj: GeometryObject :param app_obj: App :type app_obj: FlatCAMApp.App :return: :rtype: """ assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj) # ## Add properties to the object geo_obj.obj_options['type'] = 'Excellon Geometry' geo_obj.obj_options["tools_mill_tooldia"] = str(tooldia) geo_obj.obj_options["multidepth"] = app_obj.options["tools_mill_multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution # for FlatCAM is 6 decimals, # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero" for etool in tools: for drill in self.tools[etool]['drills']: buffer_value = self.tools[etool]['tooldia'] / 2 - tooldia / 2 if buffer_value == 0: geo_obj.solid_geometry.append(drill.buffer(0.0000001).exterior) else: geo_obj.solid_geometry.append(drill.buffer(buffer_value).exterior) if not geo_obj.solid_geometry: return "fail" if use_thread: def geo_thread(a_obj): a_obj.app_obj.new_object("geometry", outname, geo_init, plot=plot) # Create a promise with the new name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) else: self.app.app_obj.new_object("geometry", outname, geo_init, plot=plot) return True, "" def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False): """ Will generate an Geometry Object allowing to cut/mill a slot hole. Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. :param tools: A list of tools where the drills are to be milled or a string: "all" :type tools: :param outname: the name of the resulting Geometry object :type outname: str :param tooldia: the tool diameter to be used in creation of the milling path (Geometry Object) :type tooldia: float :param plot: if to plot the resulting object :type plot: bool :param use_thread: if to use threading for creation of the Geometry object :type use_thread: bool :return: Success/failure condition tuple (bool, str). :rtype: tuple """ # Get the tools from the list. These are keys # to self.tools if tools is None: tools = self.get_selected_tools_list() if outname is None: outname = self.obj_options["name"] + "_mill" if tooldia is None: tooldia = float(self.obj_options["slot_tooldia"]) # Sort tools by diameter. items() -> [('name', diameter), ...] # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3 sort = [] for k, v in self.tools.items(): sort.append((k, v['tooldia'])) sorted_tools = sorted(sort, key=lambda t1: t1[1]) if tools == "all": tools = [i[0] for i in sorted_tools] # List if ordered tool names. self.app.log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")) return False, "Error: No tools." for tool in tools: # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse adj_toolstable_tooldia = float('%.*f' % (self.decimals, float(tooldia))) adj_file_tooldia = float('%.*f' % (self.decimals, float(self.tools[tool]["tooldia"]))) if adj_toolstable_tooldia > adj_file_tooldia + 0.0001: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Milling tool for SLOTS is larger than hole size. Cancelled.")) return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj) # ## Add properties to the object geo_obj.obj_options['type'] = 'Excellon Geometry' geo_obj.obj_options["tools_mill_tooldia"] = str(tooldia) geo_obj.obj_options["tools_mill_multidepth"] = app_obj.options["tools_mill_multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution # for FlatCAM is 6 decimals, # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero" for m_tool in tools: for slot in self.tools[m_tool]['slots']: toolstable_tool = float('%.*f' % (self.decimals, float(tooldia))) file_tool = float('%.*f' % (self.decimals, float(self.tools[m_tool]["tooldia"]))) # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse # for the file_tool (tooldia actually) buffer_value = float(file_tool / 2) - float(toolstable_tool / 2) + 0.0001 if buffer_value == 0: start = slot[0] stop = slot[1] lines_string = LineString([start, stop]) poly = lines_string.buffer(0.0000001, int(self.geo_steps_per_circle)).exterior geo_obj.solid_geometry.append(poly) else: start = slot[0] stop = slot[1] lines_string = LineString([start, stop]) poly = lines_string.buffer(buffer_value, int(self.geo_steps_per_circle)).exterior geo_obj.solid_geometry.append(poly) if not geo_obj.solid_geometry: return "fail" if use_thread: def geo_thread(a_obj): a_obj.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot) # Create a promise with the new name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) else: self.app.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot) return True, "" def on_generate_milling_button_click(self): self.app.defaults.report_usage("excellon_on_create_milling_drills button") self.read_form() self.generate_milling_drills(use_thread=False, plot=True) def on_generate_milling_slots_button_click(self): self.app.defaults.report_usage("excellon_on_create_milling_slots_button") self.read_form() self.generate_milling_slots(use_thread=False, plot=True) def convert_units(self, units): self.app.log.debug("ExcellonObject.convert_units()") Excellon.convert_units(self, units) # factor = Excellon.convert_units(self, units) # self.obj_options['drillz'] = float(self.obj_options['drillz']) * factor # self.obj_options['travelz'] = float(self.obj_options['travelz']) * factor # self.obj_options['feedrate'] = float(self.obj_options['feedrate']) * factor # self.obj_options['feedrate_rapid'] = float(self.obj_options['feedrate_rapid']) * factor # self.obj_options['toolchangez'] = float(self.obj_options['toolchangez']) * factor # # if self.app.options["excellon_toolchangexy"] == '': # self.obj_options['toolchangexy'] = "0.0, 0.0" # else: # coords_xy = [float(eval(coord)) for coord in self.app.options["excellon_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.obj_options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) # # if self.obj_options['startz'] is not None: # self.obj_options['startz'] = float(self.obj_options['startz']) * factor # self.obj_options['endz'] = float(self.obj_options['endz']) * factor def on_solid_cb_click(self): if self.muted_ui: return self.read_form_item('solid') self.plot() def on_multicolored_cb_click(self, val): if self.muted_ui: return self.read_form_item('multicolored') self.plot() if not val: self.build_ui() def on_autoload_db_toggled(self, state): self.app.options["excellon_autoload_db"] = True if state else False def on_plot_cb_click(self): if self.muted_ui: return # self.plot() self.read_form_item('plot') self.ui_disconnect() cb_flag = self.ui.plot_cb.isChecked() for row in range(self.ui.tools_table.rowCount() - 2): table_cb = self.ui.tools_table.cellWidget(row, 5) table_cb.setChecked(True) if cb_flag else table_cb.setChecked(False) self.ui_connect() self.on_plot_cb_click_table() def on_plot_cb_click_table(self): self.ui_disconnect() check_row = 0 for tool_key in self.tools: # find the geo_plugin_table row associated with the tool_key for row in range(self.ui.tools_table.rowCount()): tool_item = int(float(self.ui.tools_table.item(row, 0).text())) if tool_item == int(tool_key): check_row = row break state = self.ui.tools_table.cellWidget(check_row, 5).isChecked() try: # suggested by an user that may fix issues when run in Linux # I don't see the reason for the .copy() but ... # TODO may need removal of the .copy() method if the reason is not found self.shapes.update_visibility(state, indexes=self.shape_indexes_dict[tool_key]).copy() except Exception: pass self.shapes.redraw() self.ui_connect() def plot(self, visible=None, kind=None): multicolored = self.ui.multicolored_cb.get_value() # 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.use_3d_engine: 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_c in range(len(r_color)): new_color += '%x' % int(r_color[idx_c] * 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 # this stays for compatibility reasons, in case we try to open old projects try: __ = iter(self.solid_geometry) except TypeError: self.solid_geometry = [self.solid_geometry] visible = visible if visible else self.ui.plot_cb.get_value() try: # Plot Excellon (All polygons?) if self.ui.solid_cb.get_value(): # plot polygons for each tool separately for tool in self.tools: # set the color here so we have one color for each tool geo_color = random_color() if multicolored: self.tools[tool]['multicolor'] = geo_color else: self.tools[tool]['multicolor'] = None # tool is a dict also for geo in self.tools[tool]["solid_geometry"]: idx = self.add_shape(shape=geo, color=geo_color if multicolored else self.outline_color, face_color=geo_color if multicolored else self.fill_color, visible=visible, layer=2) try: self.shape_indexes_dict[tool].append(idx) except KeyError: self.shape_indexes_dict[tool] = [idx] else: for tool in self.tools: for geo in self.tools[tool]['solid_geometry']: idx = self.add_shape(shape=geo.exterior, color='red', visible=visible) try: self.shape_indexes_dict[tool].append(idx) except KeyError: self.shape_indexes_dict[tool] = [idx] for ints in geo.interiors: idx = self.add_shape(shape=ints, color='orange', visible=visible) try: self.shape_indexes_dict[tool].append(idx) except KeyError: self.shape_indexes_dict[tool] = [idx] # for geo in self.solid_geometry: # self.add_shape(shape=geo.exterior, color='red', visible=visible) # for ints in geo.interiors: # self.add_shape(shape=ints, color='orange', visible=visible) self.shapes.redraw() except (ObjectDeleted, AttributeError) as e: self.app.log.debug("ExcellonObject.plot() -> %s" % str(e)) self.shapes.clear(update=True) if multicolored: self.multicolored_build_sig.emit() def on_multicolored_build(self): self.build_ui() @staticmethod def merge(exc_list, exc_final, decimals=None, fuse_tools=True, log=None): """ Merge Excellon objects found in exc_list parameter into exc_final object. Options are always copied from source . Tools are disregarded, what is taken in consideration is the unique drill diameters found as values in the exc_list tools dict's. In the reconstruction section for each unique tool diameter it will be created a tool_name to be used in the final Excellon object, exc_final. If only one object is in exc_list parameter then this function will copy that object in the exc_final :param exc_list: List or one object of ExcellonObject Objects to join. :type exc_list: list :param exc_final: Destination ExcellonObject object. :type exc_final: class :param decimals: The number of decimals to be used for diameters :type decimals: int :param fuse_tools: If True will try to fuse tools of the same diameter for the Excellon objects :type fuse_tools: bool :param log: the logging object used :return: None """ if exc_final.tools is None: exc_final.tools = {} if decimals is None: decimals = 4 decimals_exc = decimals try: flattened_list = list(itertools.chain(*exc_list)) except TypeError: flattened_list = exc_list new_tools = {} total_geo = [] toolid = 0 for exc in flattened_list: # copy options of the current excellon obj to the final excellon obj # only the last object options will survive for option in exc.obj_options: if option != 'name': try: exc_final.obj_options[option] = deepcopy(exc.obj_options[option]) except Exception: if log: log.warning("Failed to copy option.", option) for tool in exc.tools: toolid += 1 new_tools[toolid] = deepcopy(exc.tools[tool]) exc_final.tools = deepcopy(new_tools) # add the zeros and units to the exc_final object exc_final.zeros = deepcopy(exc.zeros) exc_final.units = deepcopy(exc.units) total_geo += exc.solid_geometry exc_final.solid_geometry = deepcopy(total_geo) fused_tools_dict = {} if exc_final.tools and fuse_tools: toolid = 0 for tool, tool_dict in exc_final.tools.items(): current_tooldia = float('%.*f' % (decimals_exc, tool_dict['tooldia'])) toolid += 1 # calculate all diameters in fused_tools_dict all_dia = [] if fused_tools_dict: for f_tool in fused_tools_dict: all_dia.append(float('%.*f' % (decimals_exc, fused_tools_dict[f_tool]['tooldia']))) if current_tooldia in all_dia: # find tool for current_tooldia in fuse_tools t = None for f_tool in fused_tools_dict: if fused_tools_dict[f_tool]['tooldia'] == current_tooldia: t = f_tool break if t: if 'drills' in tool_dict and tool_dict['drills']: fused_tools_dict[t]['drills'] += tool_dict['drills'] if 'slots' in tool_dict and tool_dict['slots']: fused_tools_dict[t]['slots'] += tool_dict['slots'] fused_tools_dict[t]['solid_geometry'] += tool_dict['solid_geometry'] else: fused_tools_dict[toolid] = tool_dict fused_tools_dict[toolid]['tooldia'] = current_tooldia exc_final.tools = fused_tools_dict # create the geometry for the exc_final object exc_final.create_geometry()