# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 3/10/2019 # # MIT Licence # # ########################################################## from PyQt6 import QtWidgets, QtGui, QtCore from appTool import AppTool from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, RadioSet, \ FCDoubleSpinner, FCComboBox2, OptionalInputSection, FCCheckBox from camlib import flatten_shapely_geometry import math import logging from copy import deepcopy import simplejson as json import sys from numpy import Inf from shapely import Polygon, MultiPolygon, box, Point, LineString, MultiLineString, LinearRing from shapely.ops import unary_union, linemerge from shapely.affinity import rotate from matplotlib.backend_bases import KeyEvent as mpl_key_event import gettext import appTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class CutOut(AppTool): def __init__(self, app): AppTool.__init__(self, app) self.app = app self.canvas = app.plotcanvas self.decimals = self.app.decimals # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = CutoutUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.cutting_gapsize = 0.0 self.cutting_dia = 0.0 # true if we want to repeat the gap without clicking again on the button self.repeat_gap = False self.flat_geometry = [] # this is the Geometry object generated in this class to be used for adding manual gaps self.man_cutout_obj = None # if mouse is dragging set the object True self.mouse_is_dragging = False # if mouse events are bound to local methods self.mouse_events_connected = False # event handlers references self.kp = None self.mm = None self.mr = None # hold the mouse position here self.x_pos = None self.y_pos = None # store the default data for the resulting Geometry Object self.default_data = {} # store the current cursor type to be restored after manual geo self.old_cursor_type = self.app.options["global_cursor_type"] # store the current selection shape status to be restored after manual geo self.old_selection_state = self.app.options['global_selection_shape'] # store original geometry for manual cutout self.manual_solid_geo = None # here will store the original geometry for manual cutout with mouse bytes self.mb_manual_solid_geo = None # here will store the geo rests when doing manual cutouts with mouse bites self.mb_manual_cuts = [] # here store the tool data for the Cutout Tool self.cut_tool_dict = {} def on_type_obj_changed(self, val): obj_type = {'grb': 0, 'geo': 2}[val] self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.obj_combo.setCurrentIndex(0) self.ui.obj_combo.obj_type = {"grb": "Gerber", "geo": "Geometry"}[val] if val == 'grb': self.ui.convex_box_label.setDisabled(False) self.ui.convex_box_cb.setDisabled(False) else: self.ui.convex_box_label.setDisabled(True) self.ui.convex_box_cb.setDisabled(True) def on_object_selection_changed(self, current, previous): found_idx = None for tab_idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.tabText(tab_idx) == self.ui.pluginName: found_idx = True break if found_idx: try: found_obj = current.indexes()[0].internalPointer().obj name = found_obj.obj_options['name'] kind = found_obj.kind if kind in ['gerber', 'geometry']: obj_type = {'gerber': 'grb', 'geometry': 'geo'}[kind] self.ui.type_obj_radio.set_value(obj_type) self.ui.obj_combo.set_value(name) except IndexError: pass def run(self, toggle=True): self.app.defaults.report_usage("ToolCutOut()") if toggle: # if the splitter is hidden, display it if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same found_idx = None for idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": found_idx = idx break # show the Tab if not found_idx: try: self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) except RuntimeError: self.app.ui.plugin_tab = QtWidgets.QWidget() self.app.ui.plugin_tab.setObjectName("plugin_tab") self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) self.app.ui.plugin_scroll_area = VerticalScrollArea() self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) try: if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx: # if the Tool Tab is not focused, focus on it if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab: # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) else: # else remove the Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) self.app.ui.notebook.removeTab(2) # if there are no objects loaded in the app then hide the Notebook widget if not self.app.collection.get_list(): self.app.ui.splitter.setSizes([0, 1]) except AttributeError: pass else: if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) super().run() self.set_tool_ui() self.app.ui.notebook.setTabText(2, _("Cutout")) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+X', **kwargs) def connect_signals_at_init(self): # ############################################################################# # ############################ SIGNALS ######################################## # ############################################################################# self.ui.level.toggled.connect(self.on_level_changed) self.ui.generate_cutout_btn.clicked.connect(self.on_cutout_generation) # adding tools self.ui.add_newtool_button.clicked.connect(lambda: self.on_tool_add()) self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked) self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed) self.ui.cutout_shape_cb.stateChanged.connect(self.on_cutout_shape_changed) self.ui.cutout_type_radio.activated_custom.connect(self.on_cutout_type) self.ui.man_geo_creation_btn.clicked.connect(self.on_manual_geo) self.ui.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click) self.ui.drillcut_btn.clicked.connect(self.on_drill_cut_click) self.app.proj_selection_changed.connect(self.on_object_selection_changed) self.ui.reset_button.clicked.connect(self.set_tool_ui) def set_tool_ui(self): self.clear_ui(self.layout) self.ui = CutoutUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.reset_fields() # use the current selected object and make it visible in the object combobox sel_list = self.app.collection.get_selected() if len(sel_list) == 1: active = self.app.collection.get_active() kind = active.kind if kind == 'gerber': self.ui.type_obj_radio.set_value('grb') else: self.ui.type_obj_radio.set_value('geo') # run those once so the obj_type attribute is updated for the FCComboboxes # so the last loaded object is displayed if kind == 'gerber': self.on_type_obj_changed(val='grb') else: self.on_type_obj_changed(val='geo') self.ui.obj_combo.set_value(active.obj_options['name']) else: kind = 'gerber' self.ui.type_obj_radio.set_value('grb') # run those once so the obj_type attribute is updated for the FCComboboxes # so the last loaded object is displayed if kind == 'gerber': self.on_type_obj_changed(val='grb') else: self.on_type_obj_changed(val='geo') # init the working variables self.default_data.clear() kind = 'geometry' for option in self.app.options: if option.find(kind + "_") == 0: oname = option[len(kind) + 1:] self.default_data[oname] = self.app.options[option] if option.find('tools_') == 0: self.default_data[option] = self.app.options[option] self.ui.gaptype_combo.set_value(self.app.options["tools_cutout_gap_type"]) self.ui.on_gap_type_radio(self.ui.gaptype_combo.get_value()) # add a default tool self.ui.dia.set_value(float(self.app.options["tools_cutout_tooldia"])) tool_dia = float(self.app.options["tools_cutout_tooldia"]) self.on_tool_add(custom_dia=tool_dia) # set as default the automatic adding of gaps self.ui.cutout_type_radio.set_value('a') self.on_cutout_type(val='a') self.ui.cutout_shape_cb.set_value(False) self.on_cutout_shape_changed(self.ui.cutout_shape_cb.get_value()) # set the Cut By Drilling parameters self.ui.drill_dia_entry.set_value(float(self.app.options["tools_cutout_drill_dia"])) self.ui.drill_pitch_entry.set_value(float(self.app.options["tools_cutout_drill_pitch"])) self.ui.drill_margin_entry.set_value(float(self.app.options["tools_cutout_drill_margin"])) # Show/Hide Advanced Options app_mode = self.app.options["global_app_level"] self.change_level(app_mode) 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.convex_box_label.hide() self.ui.convex_box_cb.hide() # Add Tool section # self.ui.tool_sel_label.hide() self.ui.add_newtool_button.hide() self.ui.addtool_from_db_btn.hide() # Tool parameters section if self.cut_tool_dict: tool_data = self.cut_tool_dict['data'] tool_data['tools_cutout_convexshape'] = False tool_data['tools_cutout_gap_type'] = 0 # "Basic Type of Gap" self.ui.gaptype_label.hide() self.ui.gaptype_combo.hide() self.ui.cutout_type_label.hide() self.ui.cutout_type_radio.hide() self.ui.cutout_type_radio.set_value('a') self.ui.separator_line.hide() self.ui.drill_cut_frame.hide() self.ui.title_drillcut_label.hide() self.ui.drillcut_btn.hide() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.level.setStyleSheet(""" QToolButton { color: red; } """) self.ui.convex_box_label.show() self.ui.convex_box_cb.show() # Add Tool section # self.ui.tool_sel_label.show() self.ui.add_newtool_button.show() self.ui.addtool_from_db_btn.show() # Tool parameters section if self.cut_tool_dict: app_defaults = self.app.options tool_data = self.cut_tool_dict['data'] tool_data['tools_cutout_convexshape'] = app_defaults['tools_cutout_convexshape'] tool_data['tools_cutout_gap_type'] = app_defaults['tools_cutout_gap_type'] self.ui.gaptype_label.show() self.ui.gaptype_combo.show() self.ui.cutout_type_label.show() self.ui.cutout_type_radio.show() self.ui.cutout_type_radio.set_value('a') self.ui.separator_line.show() self.ui.drill_cut_frame.show() self.ui.title_drillcut_label.show() self.ui.drillcut_btn.show() if self.cut_tool_dict: tool_data = self.cut_tool_dict['data'] self.ui.on_gap_type_radio(tool_data['tools_cutout_gap_type']) def update_ui(self, tool_dict): self.ui.obj_kind_combo.set_value(self.default_data["tools_cutout_kind"]) self.ui.big_cursor_cb.set_value(self.default_data['tools_cutout_big_cursor']) # Entries that may be updated from database self.ui.margin.set_value(float(tool_dict["tools_cutout_margin"])) self.ui.gapsize.set_value(float(tool_dict["tools_cutout_gapsize"])) self.ui.gaptype_combo.set_value(tool_dict["tools_cutout_gap_type"]) self.on_cutout_type(self.ui.gaptype_combo.get_value()) self.ui.thin_depth_entry.set_value(float(tool_dict["tools_cutout_gap_depth"])) self.ui.mb_dia_entry.set_value(float(tool_dict["tools_cutout_mb_dia"])) self.ui.mb_spacing_entry.set_value(float(tool_dict["tools_cutout_mb_spacing"])) self.ui.convex_box_cb.set_value(tool_dict['tools_cutout_convexshape']) self.ui.gaps.set_value(tool_dict["tools_cutout_gaps_ff"]) self.ui.cutz_entry.set_value(float(tool_dict["tools_cutout_z"])) self.ui.mpass_cb.set_value(bool(tool_dict["tools_cutout_mdepth"])) self.ui.maxdepth_entry.set_value(float(tool_dict["tools_cutout_depthperpass"])) def on_cutout_type(self, val): if val == 'a': self.ui.gaps_label.show() self.ui.gaps.show() self.ui.generate_cutout_btn.show() self.ui.man_geo_creation_btn.hide() self.ui.man_gaps_creation_btn.hide() self.ui.man_frame.hide() else: self.ui.gaps_label.hide() self.ui.gaps.hide() self.ui.generate_cutout_btn.hide() self.ui.man_geo_creation_btn.show() self.ui.man_gaps_creation_btn.show() self.ui.man_frame.show() def on_cutout_shape_changed(self, state): if state: self.ui.generate_cutout_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/rectangle32.png')) self.ui.man_geo_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/rectangle32.png')) self.ui.cutout_shape_cb.setText('%s' % _("Rectangular")) else: self.ui.generate_cutout_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/irregular32.png')) self.ui.man_geo_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/irregular32.png')) self.ui.cutout_shape_cb.setText('%s' % _("Any")) def on_tool_add(self, custom_dia=None): self.blockSignals(True) filename = self.app.tools_database_path() new_tools_dict = deepcopy(self.default_data) updated_tooldia = None # determine the new tool diameter if custom_dia is None: tool_dia = self.ui.dia.get_value() else: tool_dia = custom_dia if tool_dia is None or tool_dia == 0: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter with non-zero value, " "in Float format.")) self.blockSignals(False) return truncated_tooldia = self.app.dec_format(tool_dia, self.decimals) # load the database tools from the file try: with open(filename) as f: tools = f.read() except IOError: self.app.log.error("Could not load tools DB file.") self.app.inform.emit('[ERROR] %s' % _("Could not load Tools DB file.")) self.blockSignals(False) self.on_tool_default_add(dia=tool_dia) return try: # store here the tools from Tools Database when searching in Tools Database tools_db_dict = json.loads(tools) except Exception: e = sys.exc_info()[0] self.app.log.error(str(e)) self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file.")) self.blockSignals(False) self.on_tool_default_add(dia=tool_dia) return tool_found = 0 # look in database tools for db_tool, db_tool_val in tools_db_dict.items(): db_tooldia = db_tool_val['tooldia'] low_limit = float(db_tool_val['data']['tol_min']) high_limit = float(db_tool_val['data']['tol_max']) # we need only tool marked for Cutout Tool if db_tool_val['data']['tool_target'] != _('Cutout'): continue # if we find a tool with the same diameter in the Tools DB just update it's data if truncated_tooldia == db_tooldia: tool_found += 1 for d in db_tool_val['data']: if d.find('tools_cutout_') == 0: new_tools_dict[d] = db_tool_val['data'][d] elif d.find('tools_') == 0: # don't need data for other App Tools; this tests after 'tools_cutout_' continue else: new_tools_dict[d] = db_tool_val['data'][d] # search for a tool that has a tolerance that the tool fits in elif high_limit >= truncated_tooldia >= low_limit: tool_found += 1 updated_tooldia = db_tooldia for d in db_tool_val['data']: if d.find('tools_cutout_') == 0: new_tools_dict[d] = db_tool_val['data'][d] elif d.find('tools_') == 0: # don't need data for other App Tools; this tests after 'tools_cutout_' continue else: new_tools_dict[d] = db_tool_val['data'][d] # test we found a suitable tool in Tools Database or if multiple ones if tool_found == 0: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool not in Tools Database. Adding a default tool.")) self.on_tool_default_add() self.blockSignals(False) return if tool_found > 1: self.app.inform.emit( '[WARNING_NOTCL] %s' % _("Cancelled.\n" "Multiple tools for one tool diameter found in Tools Database.")) self.blockSignals(False) return new_tools_dict["tools_cutout_z"] = deepcopy(new_tools_dict["tools_mill_cutz"]) new_tools_dict["tools_cutout_mdepth"] = deepcopy(new_tools_dict["tools_mill_multidepth"]) new_tools_dict["tools_cutout_depthperpass"] = deepcopy(new_tools_dict["tools_mill_depthperpass"]) new_tdia = deepcopy(updated_tooldia) if updated_tooldia is not None else deepcopy(truncated_tooldia) self.cut_tool_dict.update({ 'tooldia': new_tdia, 'data': deepcopy(new_tools_dict), 'solid_geometry': [] }) self.update_ui(new_tools_dict) self.blockSignals(False) self.app.inform.emit('[success] %s' % _("Updated tool from Tools Database.")) def on_tool_default_add(self, dia=None, muted=None): dia = dia if dia else str(self.app.options["tools_cutout_tooldia"]) # init the working variables self.default_data.clear() kind = 'geometry' for option in self.app.options: if option.find(kind + "_") == 0: oname = option[len(kind) + 1:] self.default_data[oname] = self.app.options[option] if option.find('tools_') == 0: self.default_data[option] = self.app.options[option] self.cut_tool_dict.update({ 'tooldia': dia, 'data': deepcopy(self.default_data), 'solid_geometry': [] }) self.update_ui(self.default_data) if muted is None: self.app.inform.emit('[success] %s' % _("Default tool added.")) def on_cutout_tool_add_from_db_executed(self, tool): """ Here add the tool from DB in the selected geometry object :return: """ if tool['data']['tool_target'] not in [0, 6]: # [General, Cutout Tool] for idx in range(self.app.ui.plot_tab_area.count()): if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"): wdg = self.app.ui.plot_tab_area.widget(idx) wdg.deleteLater() self.app.ui.plot_tab_area.removeTab(idx) self.app.inform.emit('[ERROR_NOTCL] %s' % _("Selected tool can't be used here. Pick another.")) return tool_from_db = deepcopy(self.default_data) tool_from_db.update(tool) tool_from_db['data']["tools_cutout_tooldia"] = deepcopy(tool["tooldia"]) tool_from_db['data']["tools_cutout_z"] = deepcopy(tool_from_db['data']["tools_mill_cutz"]) tool_from_db['data']["tools_cutout_mdepth"] = deepcopy(tool_from_db['data']["tools_mill_multidepth"]) tool_from_db['data']["tools_cutout_depthperpass"] = deepcopy(tool_from_db['data']["tools_mill_depthperpass"]) self.cut_tool_dict.update(tool_from_db) self.cut_tool_dict['solid_geometry'] = [] self.update_ui(tool_from_db['data']) self.ui.dia.set_value(float(tool_from_db['data']["tools_cutout_tooldia"])) for idx in range(self.app.ui.plot_tab_area.count()): if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"): wdg = self.app.ui.plot_tab_area.widget(idx) wdg.deleteLater() self.app.ui.plot_tab_area.removeTab(idx) self.app.inform.emit('[success] %s' % _("Tool updated from Tools Database.")) 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 """ tooldia = float(tool['tooldia']) truncated_tooldia = self.app.dec_format(tooldia, self.decimals) self.cutout_tools.update({ 1: { 'tooldia': truncated_tooldia, 'data': deepcopy(tool['data']), 'solid_geometry': [] } }) self.cutout_tools[1]['data']['name'] = '_cutout' return 1 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 ret_val = self.app.on_tools_database(source='cutout') if ret_val == 'fail': return self.app.tools_db_tab.ok_to_add = True self.app.tools_db_tab.ui.buttons_frame.hide() self.app.tools_db_tab.ui.add_tool_from_db.show() self.app.tools_db_tab.ui.cancel_tool_from_db.show() def on_cutout_generation(self): cutout_rect_shape = self.ui.cutout_shape_cb.get_value() if cutout_rect_shape: self.on_rectangular_cutout() else: self.on_freeform_cutout() def on_freeform_cutout(self): self.app.log.debug("CutOut.on_freeform_cutout() is running....") name = self.ui.obj_combo.currentText() # Get source object. try: cutout_obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_freeform_cutout() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return "Could not retrieve object: %s" % name if cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s' % _("There is no object selected for Cutout.\nSelect one and try again.")) return cut_dia = self.ui.dia.get_value() if 0 in {cut_dia}: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) try: kind = self.ui.obj_kind_combo.get_value() except ValueError: return margin = self.ui.margin.get_value() try: gaps = self.ui.gaps.get_value() except TypeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry.")) return if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Gaps value can be only one of: 'None', 'lr', 'tb', '2lr', '2tb', 4 or 8.\n" "Fill in a correct value and retry.")) return with self.app.proc_container.new("Generating Cutout ..."): formatted_name = cutout_obj.obj_options["name"].rpartition('.')[0] if formatted_name != '': outname = "%s_cutout" % formatted_name else: outname = "%s_cutout" % cutout_obj.obj_options["name"] self.app.collection.promise(outname) has_mouse_bites = True if self.ui.gaptype_combo.get_value() == 2 else False # "mouse bytes" outname_exc = "%s_mouse_bites" % formatted_name if has_mouse_bites is True: self.app.collection.promise(outname_exc) def job_thread(app_obj): solid_geo = [] gaps_solid_geo = [] mouse_bites_geo = [] convex_box = self.ui.convex_box_cb.get_value() gapsize = self.ui.gapsize.get_value() # real gapsize has to be bigger because it will be made less by the cutting tool diameter gapsize = gapsize + abs(cut_dia) mb_dia = self.ui.mb_dia_entry.get_value() mb_buff_val = mb_dia / 2.0 mb_spacing = self.ui.mb_spacing_entry.get_value() gap_type = self.ui.gaptype_combo.get_value() thin_entry = self.ui.thin_depth_entry.get_value() if cutout_obj.kind == 'gerber': if isinstance(cutout_obj.solid_geometry, list): cutout_obj.solid_geometry = unary_union(cutout_obj.solid_geometry) try: if convex_box: object_geo = cutout_obj.solid_geometry.convex_hull else: object_geo = cutout_obj.solid_geometry except Exception as err: self.app.log.error("CutOut.on_freeform_cutout().geo_init() --> %s" % str(err)) object_geo = cutout_obj.solid_geometry else: if cutout_obj.multigeo is False: object_geo = cutout_obj.solid_geometry else: # first tool in the tools dict t_first = list(cutout_obj.tools.keys())[0] object_geo = cutout_obj.tools[t_first]['solid_geometry'] if kind == 'single': object_geo = unary_union(object_geo) # for geo in object_geo: if cutout_obj.kind == 'gerber': if isinstance(object_geo, MultiPolygon): x0, y0, x1, y1 = object_geo.bounds object_geo = box(x0, y0, x1, y1) if margin >= 0: geo_buf = object_geo.buffer(margin + abs(cut_dia / 2)) geo = geo_buf.exterior else: geo_buf = object_geo.buffer(-margin + abs(cut_dia / 2)) geo = unary_union(geo_buf.interiors) else: if isinstance(object_geo, (MultiPolygon, MultiLineString)): x0, y0, x1, y1 = object_geo.bounds object_geo = box(x0, y0, x1, y1) if isinstance(object_geo, (LinearRing, LineString)): object_geo = Polygon(object_geo) if margin >= 0: geo_buf = object_geo.buffer(margin) if geo_buf.is_empty: geo_buf = object_geo.buffer(margin + 0.0000001) geo = geo_buf.exterior else: geo_buf = object_geo.buffer(0.0000001) geo_ext = geo_buf.exterior buff_geo_ext = geo_ext.buffer(-margin) geo = unary_union(buff_geo_ext.interiors) if geo.is_empty: self.app.log.debug("Cutout.on_freeform_cutout() -> Empty geometry.") self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return 'fail' try: solid_geo, rest_geo = self.any_cutout_handler(geo, abs(cut_dia), gaps, gapsize, margin) except Exception as err: self.app.log.error("Cutout.on_freeform_cutout() -> %s" % str(err)) self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return if gap_type == 1 and thin_entry != 0: # "Thin gaps" gaps_solid_geo = rest_geo else: object_geo = flatten_shapely_geometry(object_geo) for geom_struct in object_geo: if cutout_obj.kind == 'gerber': if margin >= 0: geom_struct = (geom_struct.buffer(margin + abs(cut_dia / 2))).exterior else: geom_struct_buff = geom_struct.buffer(-margin + abs(cut_dia / 2)) geom_struct = geom_struct_buff.interiors else: if margin >= 0: geo_buf = geom_struct.buffer(margin) if geo_buf.is_empty: geo_buf = geom_struct.buffer(margin + 0.0000001) geom_struct = geo_buf.exterior else: geo_buf = geom_struct.buffer(0.0000001) geo_ext = geo_buf.exterior buff_geo_ext = geo_ext.buffer(-margin) if isinstance(buff_geo_ext, MultiPolygon): self.app.log.debug( "Cutout.on_freeform_cutout() -> The source geometry cannot be used.") self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) unary_union(buff_geo_ext) else: geom_struct = unary_union(buff_geo_ext.interiors) try: c_geo, r_geo = self.any_cutout_handler(geom_struct, abs(cut_dia), gaps, gapsize, margin) except Exception as err: self.app.log.error("Cutout.on_freeform_cutout() -> %s" % str(err)) self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) continue solid_geo += c_geo if gap_type == 1 and thin_entry != 0: # "Thin gaps" gaps_solid_geo += r_geo if not solid_geo: self.app.log.debug("Cutout.on_freeform_cutout() -> Empty solid geometry.") self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return "fail" try: solid_geo = linemerge(solid_geo) except Exception: # there ar enot lines but polygons pass # If it has mouse bytes if has_mouse_bites is True: gapsize -= abs(cut_dia) / 2 mb_object_geo = deepcopy(object_geo) if kind == 'single': mb_object_geo = unary_union(mb_object_geo) # for geo in object_geo: if cutout_obj.kind == 'gerber': if isinstance(mb_object_geo, MultiPolygon): x0, y0, x1, y1 = mb_object_geo.bounds mb_object_geo = box(x0, y0, x1, y1) if margin >= 0: geo_buf = mb_object_geo.buffer(margin + mb_buff_val) else: geo_buf = mb_object_geo.buffer(margin - mb_buff_val) mb_geo = geo_buf.exterior else: if isinstance(mb_object_geo, MultiPolygon): x0, y0, x1, y1 = mb_object_geo.bounds mb_object_geo = box(x0, y0, x1, y1) geo_buf = mb_object_geo.buffer(0.0000001) mb_geo = geo_buf.exterior __, rest_geo = self.any_cutout_handler(mb_geo, abs(cut_dia), gaps, gapsize, margin) mouse_bites_geo = rest_geo else: mb_object_geo = flatten_shapely_geometry(mb_object_geo) for mb_geom_struct in mb_object_geo: if cutout_obj.kind == 'gerber': if margin >= 0: mb_geom_struct = mb_geom_struct.buffer(margin + mb_buff_val) mb_geom_struct = mb_geom_struct.exterior else: mb_geom_struct = mb_geom_struct.buffer(-margin + mb_buff_val) mb_geom_struct = mb_geom_struct.interiors __, mb_r_geo = self.any_cutout_handler(mb_geom_struct, abs(cut_dia), gaps, gapsize, margin) mouse_bites_geo += mb_r_geo # list of Shapely Points to mark the drill points centers holes = [] for line in mouse_bites_geo: calc_len = 0 while calc_len <= line.length: holes.append(line.interpolate(calc_len)) calc_len += mb_dia + mb_spacing def geo_init(geo_obj, app_object): geo_obj.multigeo = True geo_obj.solid_geometry = deepcopy(solid_geo) geo_obj.tools[1] = deepcopy(self.cut_tool_dict) geo_obj.tools[1]['tooldia'] = str(abs(cut_dia)) geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry geo_obj.tools[1]['data']['name'] = outname geo_obj.tools[1]['data']['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.tools[1]['data']['tools_mill_cutz'] = self.ui.cutz_entry.get_value() geo_obj.tools[1]['data']['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() geo_obj.tools[1]['data']['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() # if there are "thin gaps" if gaps_solid_geo: geo_obj.tools[99] = deepcopy(self.cut_tool_dict) geo_obj.tools[99]['tooldia'] = str(abs(cut_dia)) geo_obj.tools[99]['solid_geometry'] = gaps_solid_geo geo_obj.tools[99]['data']['name'] = outname geo_obj.tools[99]['data']['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.tools[99]['data']['tools_mill_cutz'] = self.ui.thin_depth_entry.get_value() geo_obj.tools[99]['data']['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() geo_obj.tools[99]['data']['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() # plot this tool in a different color geo_obj.tools[99]['data']['override_color'] = "#29a3a3fa" xmin, ymin, xmax, ymax = CutOut.recursive_bounds(geo_obj.solid_geometry) geo_obj.obj_options['xmin'] = xmin geo_obj.obj_options['ymin'] = ymin geo_obj.obj_options['xmax'] = xmax geo_obj.obj_options['ymax'] = ymax geo_obj.obj_options['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.obj_options['tools_mill_cutz'] = self.ui.cutz_entry.get_value() geo_obj.obj_options['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() geo_obj.obj_options['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() def excellon_init(exc_obj, app_o): if not holes: return 'fail' tools = { 1: { "tooldia": mb_dia, "drills": holes, "solid_geometry": [] } } exc_obj.multigeo = True exc_obj.tools = tools exc_obj.create_geometry() exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.obj_options['name'], local_use=exc_obj, filename=None, use_thread=False) # calculate the bounds xmin, ymin, xmax, ymax = CutOut.recursive_bounds(exc_obj.solid_geometry) exc_obj.obj_options['xmin'] = xmin exc_obj.obj_options['ymin'] = ymin exc_obj.obj_options['xmax'] = xmax exc_obj.obj_options['ymax'] = ymax try: if self.ui.gaptype_combo.get_value() == 2: # "mouse bytes" ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init, autoselected=False) if ret == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed.")) app_obj.should_we_save = True ret = app_obj.app_obj.new_object('geometry', outname, geo_init, autoselected=False) if ret == 'fail': self.app.log.debug("Cutout.on_freeform_cutout() -> Failure in creating an Geometry object..") app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return # cutout_obj.plot(plot_tool=1) app_obj.inform.emit('[success] %s' % _("Any-form Cutout operation finished.")) # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) app_obj.should_we_save = True except Exception as ee: app_obj.app.log.error(str(ee)) self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) def any_cutout_handler(self, geom, cut_diameter, gaps, gapsize, margin): r_temp_geo = [] initial_geo = deepcopy(geom) # Get min and max data for each object as we just cut rectangles across X or Y xxmin, yymin, xxmax, yymax = CutOut.recursive_bounds(geom) px = 0.5 * (xxmax - xxmin) + xxmin # center X py = 0.5 * (yymax - yymin) + yymin # center Y lenx = (xxmax - xxmin) + (margin * 2) leny = (yymax - yymin) + (margin * 2) if gaps.lower() != 'none': if gaps == '8' or gaps in ['2LR', '2lr']: points = ( xxmin - (gapsize + cut_diameter), # botleft_x py - (gapsize / 2) + leny / 4, # botleft_y xxmax + (gapsize + cut_diameter), # topright_x py + (gapsize / 2) + leny / 4 # topright_y ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) points = ( xxmin - (gapsize + cut_diameter), py - (gapsize / 2) - leny / 4, xxmax + (gapsize + cut_diameter), py + (gapsize / 2) - leny / 4 ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) if gaps == '8' or gaps in ['2TB', '2tb']: points = ( px - (gapsize / 2) + lenx / 4, yymin - (gapsize + cut_diameter), px + (gapsize / 2) + lenx / 4, yymax + (gapsize + cut_diameter) ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) points = ( px - (gapsize / 2) - lenx / 4, yymin - (gapsize + cut_diameter), px + (gapsize / 2) - lenx / 4, yymax + (gapsize + cut_diameter) ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) if gaps == '4' or gaps in ['LR', 'lr']: points = ( xxmin - (gapsize + cut_diameter), py - (gapsize / 2), xxmax + (gapsize + cut_diameter), py + (gapsize / 2) ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) if gaps == '4' or gaps in ['TB', 'tb']: points = ( px - (gapsize / 2), yymin - (gapsize + cut_diameter), px + (gapsize / 2), yymax + (gapsize + cut_diameter) ) geom = self.subtract_poly_from_geo(geom, points) r_temp_geo.append( self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3])) ) try: # for g in geom: # proc_geometry.append(g) work_geom = geom.geoms if isinstance(geom, (MultiPolygon, MultiLineString)) else geom proc_geometry = [g for g in work_geom if not g.is_empty] except TypeError: # proc_geometry.append(geom) proc_geometry = [geom] r_temp_geo = CutOut.flatten(r_temp_geo) rest_geometry = [g for g in r_temp_geo if g and not g.is_empty] return proc_geometry, rest_geometry def on_rectangular_cutout(self): self.app.log.debug("CutOut.on_rectangular_cutout() is running....") name = self.ui.obj_combo.currentText() # Get source object. try: cutout_obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_rectangular_cutout() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return "Could not retrieve object: %s" % name if cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name))) return cut_dia_val = float(self.ui.dia.get_value()) if 0 in {cut_dia_val}: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) try: kind = self.ui.obj_kind_combo.get_value() except ValueError: return margin_val = self.ui.margin.get_value() try: gaps_val = self.ui.gaps.get_value() except TypeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry.")) return if gaps_val not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']: msg = '[WARNING_NOTCL] %s' % _("Gaps value can be only one of: 'None', 'lr', 'tb', '2lr', '2tb', 4 or 8.\n" "Fill in a correct value and retry.") self.app.inform.emit(msg) return gapsize_val = self.ui.gapsize.get_value() mb_dia_val = self.ui.mb_dia_entry.get_value() mb_spacing_val = self.ui.mb_spacing_entry.get_value() gap_type_val = self.ui.gaptype_combo.get_value() thin_entry_val = self.ui.thin_depth_entry.get_value() has_mouse_bites_val = True if self.ui.gaptype_combo.get_value() == 2 else False # "mouse bytes" formatted_name = cutout_obj.obj_options["name"].rpartition('.')[0] outname = "%s_cutout" % formatted_name if formatted_name != '' else "%s_cutout" % cutout_obj.obj_options["name"] self.app.collection.promise(outname) outname_exc = cutout_obj.obj_options["name"] + "_mouse_bites" if has_mouse_bites_val is True: self.app.collection.promise(outname_exc) with self.app.proc_container.new("Generating Cutout ..."): def job_thread(self_c, app_obj, gaps, gapsize, gap_type, cut_dia, margin, mb_dia, mb_spacing, thin_entry, has_mouse_bites): solid_geo = [] gaps_solid_geo = [] mouse_bites_geo = [] mb_buff_val = mb_dia / 2.0 # real gapsize has to be bigger because it will be made less by the cutting tool diameter gapsize = gapsize + abs(cut_dia) if cutout_obj.multigeo is False: object_geo = cutout_obj.solid_geometry else: # first tool in the tools dict t_first = list(cutout_obj.tools.keys())[0] object_geo = cutout_obj.tools[t_first]['solid_geometry'] if kind == 'single': # fuse the lines object_geo = unary_union(object_geo) # if isinstance(object_geo, (MultiPolygon, MultiLineString)): # x0, y0, x1, y1 = object_geo.bounds # geo = box(x0, y0, x1, y1) # if isinstance(object_geo, (LinearRing, LineString)): # geo = Polygon(object_geo) xmin, ymin, xmax, ymax = object_geo.bounds geo = box(xmin, ymin, xmax, ymax) # if Geometry then cut through the geometry # if Gerber create a buffer at a distance if cutout_obj.kind == 'gerber': if margin >= 0: work_margin = margin + abs(cut_dia / 2) else: work_margin = margin - abs(cut_dia / 2) geo = geo.buffer(work_margin) else: if margin >= 0: geo_buf = geo.buffer(margin) if geo_buf.is_empty: geo_buf = geo.buffer(margin + 0.0000001) geo = geo_buf.exterior else: geo_buf = geo.buffer(0.0000001) geo_ext = geo_buf.exterior buff_geo_ext = geo_ext.buffer(-margin) geo = unary_union(buff_geo_ext.interiors) # w_gapsize = gapsize - abs(cut_dia) solid_geo = self.rect_cutout_handler(geo, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) if gap_type == 1 and thin_entry != 0: # "Thin gaps" gaps_solid_geo = self_c.subtract_geo(geo, deepcopy(solid_geo)) else: if cutout_obj.kind == 'geometry': object_geo = flatten_shapely_geometry(object_geo) for geom_struct in object_geo: geom_struct = unary_union(geom_struct) xmin, ymin, xmax, ymax = geom_struct.bounds geom_struct = box(xmin, ymin, xmax, ymax) if margin >= 0: geo_buf = geom_struct.buffer(margin) if geo_buf.is_empty: geo_buf = geom_struct.buffer(margin + 0.0000001) geom_struct = geo_buf.exterior else: geo_buf = geom_struct.buffer(0.0000001) geo_ext = geo_buf.exterior buff_geo_ext = geo_ext.buffer(-margin) geom_struct = unary_union(buff_geo_ext.interiors) c_geo = self.rect_cutout_handler(geom_struct, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) solid_geo += c_geo if gap_type == 1 and thin_entry != 0: # "Thin gaps" try: gaps_solid_geo += self_c.subtract_geo(geom_struct, c_geo) except TypeError: gaps_solid_geo.append(self_c.subtract_geo(geom_struct, c_geo)) elif cutout_obj.kind == 'gerber' and margin >= 0: object_geo = flatten_shapely_geometry(object_geo) for geom_struct in object_geo: geom_struct = unary_union(geom_struct) xmin, ymin, xmax, ymax = geom_struct.bounds geom_struct = box(xmin, ymin, xmax, ymax) geom_struct = geom_struct.buffer(margin + abs(cut_dia / 2)) c_geo = self.rect_cutout_handler(geom_struct, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) solid_geo += c_geo if gap_type == 1 and thin_entry != 0: # "Thin gaps" try: gaps_solid_geo += self_c.subtract_geo(geom_struct, c_geo) except TypeError: gaps_solid_geo.append(self_c.subtract_geo(geom_struct, c_geo)) elif cutout_obj.kind == 'gerber' and margin < 0: app_obj.log.error("Rectangular cutout with negative margin is not possible.") mesg = '[ERROR_NOTCL] %s' % _("Failed.") app_obj.inform.emit(mesg) return "fail" if not solid_geo: self.app.log.debug("Cutout.on_rectangular_cutout() -> Empty solid geometry.") app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return "fail" try: solid_geo = linemerge(solid_geo) except Exception: # there are not lines but polygon pass if has_mouse_bites is True: gapsize -= abs(cut_dia) / 2 mb_object_geo = deepcopy(object_geo) if kind == 'single': # fuse the lines mb_object_geo = unary_union(mb_object_geo) xmin, ymin, xmax, ymax = mb_object_geo.bounds mb_geo = box(xmin, ymin, xmax, ymax) # if Gerber create a buffer at a distance # if Geometry then cut through the geometry if cutout_obj.kind == 'gerber': if margin >= 0: mb_geo = mb_geo.buffer(margin + mb_buff_val) else: mb_geo = mb_geo.buffer(margin - mb_buff_val) else: mb_geo = mb_geo.buffer(0.0000001) mb_solid_geo = self.rect_cutout_handler(mb_geo, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) mouse_bites_geo = self_c.subtract_geo(mb_geo, mb_solid_geo) else: if cutout_obj.kind == 'geometry': mb_object_geo = flatten_shapely_geometry(mb_object_geo) for mb_geom_struct in mb_object_geo: mb_geom_struct = unary_union(mb_geom_struct) xmin, ymin, xmax, ymax = mb_geom_struct.bounds mb_geom_struct = box(xmin, ymin, xmax, ymax) c_geo = self.rect_cutout_handler(mb_geom_struct, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) solid_geo += c_geo try: mouse_bites_geo += self_c.subtract_geo(mb_geom_struct, c_geo) except TypeError: mouse_bites_geo.append(self_c.subtract_geo(mb_geom_struct, c_geo)) elif cutout_obj.kind == 'gerber' and margin >= 0: mb_object_geo = flatten_shapely_geometry(mb_object_geo) for mb_geom_struct in mb_object_geo: mb_geom_struct = unary_union(mb_geom_struct) xmin, ymin, xmax, ymax = mb_geom_struct.bounds mb_geom_struct = box(xmin, ymin, xmax, ymax) mb_geom_struct = mb_geom_struct.buffer(margin + mb_buff_val) c_geo = self.rect_cutout_handler(mb_geom_struct, abs(cut_dia), gaps, gapsize, margin, xmin, ymin, xmax, ymax) solid_geo += c_geo try: mouse_bites_geo += self_c.subtract_geo(mb_geom_struct, c_geo) except TypeError: mouse_bites_geo.append(self_c.subtract_geo(mb_geom_struct, c_geo)) elif cutout_obj.kind == 'gerber' and margin < 0: app_obj.log.error("Rectangular cutout with negative margin is not possible.") msg2 = '[ERROR_NOTCL] %s' % _("Failed.") app_obj.inform.emit(msg2) return "fail" # list of Shapely Points to mark the drill points centers holes = [] for line in mouse_bites_geo: calc_len = 0 while calc_len <= line.length: holes.append(line.interpolate(calc_len)) calc_len += mb_dia + mb_spacing def geo_init(geo_obj, application_obj): geo_obj.multigeo = True geo_obj.solid_geometry = deepcopy(solid_geo) geo_obj.tools[1] = deepcopy(self_c.cut_tool_dict) geo_obj.tools[1]['tooldia'] = str(abs(cut_dia)) geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry geo_obj.tools[1]['data']['name'] = outname geo_obj.tools[1]['data']['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.tools[1]['data']['tools_mill_cutz'] = self_c.ui.cutz_entry.get_value() geo_obj.tools[1]['data']['tools_mill_multidepth'] = self_c.ui.mpass_cb.get_value() geo_obj.tools[1]['data']['tools_mill_depthperpass'] = self_c.ui.maxdepth_entry.get_value() if gaps_solid_geo: geo_obj.tools[99] = deepcopy(self_c.cut_tool_dict) geo_obj.tools[99]['tooldia'] = str(abs(cut_dia)) geo_obj.tools[99]['solid_geometry'] = gaps_solid_geo geo_obj.tools[99]['data']['name'] = outname geo_obj.tools[99]['data']['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.tools[99]['data']['tools_mill_cutz'] = self_c.ui.thin_depth_entry.get_value() geo_obj.tools[99]['data']['tools_mill_multidepth'] = self_c.ui.mpass_cb.get_value() geo_obj.tools[99]['data']['tools_mill_depthperpass'] = self_c.ui.maxdepth_entry.get_value() geo_obj.tools[99]['data']['override_color'] = "#29a3a3fa" xmin, ymin, xmax, ymax = CutOut.recursive_bounds(geo_obj.solid_geometry) geo_obj.obj_options['xmin'] = xmin geo_obj.obj_options['ymin'] = ymin geo_obj.obj_options['xmax'] = xmax geo_obj.obj_options['ymax'] = ymax geo_obj.obj_options['tools_mill_tooldia'] = str(abs(cut_dia)) geo_obj.obj_options['tools_mill_cutz'] = self_c.ui.cutz_entry.get_value() geo_obj.obj_options['tools_mill_multidepth'] = self_c.ui.mpass_cb.get_value() geo_obj.obj_options['tools_mill_depthperpass'] = self_c.ui.maxdepth_entry.get_value() def excellon_init(exc_obj, app_o): if not holes: return 'fail' tools = { 1: { "tooldia": mb_dia, "drills": holes, "solid_geometry": [] } } exc_obj.tools = tools exc_obj.create_geometry() exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.obj_options['name'], local_use=exc_obj, filename=None, use_thread=False) # calculate the bounds e_xmin, e_ymin, e_xmax, e_ymax = CutOut.recursive_bounds(exc_obj.solid_geometry) exc_obj.obj_options['xmin'] = e_xmin exc_obj.obj_options['ymin'] = e_ymin exc_obj.obj_options['xmax'] = e_xmax exc_obj.obj_options['ymax'] = e_ymax try: if self_c.ui.gaptype_combo.get_value() == 2: # "mouse bytes" ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init, autoselected=False) if ret == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed.")) ret = app_obj.app_obj.new_object('geometry', outname, geo_init, autoselected=False) if ret == 'fail': self.app.log.debug("Cutout.on_rectangular_cutout() -> Could not create a Geometry object.") app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return # cutout_obj.plot(plot_tool=1) app_obj.inform.emit('[success] %s' % _("Rectangular CutOut operation finished.")) # self_c.app.ui.notebook.setCurrentWidget(self_c.app.ui.project_tab) app_obj.should_we_save = True except Exception as ee: app_obj.log.error(str(ee)) self.app.worker_task.emit( { 'fcn': job_thread, 'params': [ self, self.app, gaps_val, gapsize_val, gap_type_val, cut_dia_val, margin_val, mb_dia_val, mb_spacing_val, thin_entry_val, has_mouse_bites_val ] }) def rect_cutout_handler(self, geom, cut_dia, gaps, gapsize, margin, xmin, ymin, xmax, ymax): px = (0.5 * (xmax - xmin)) + xmin # center X py = (0.5 * (ymax - ymin)) + ymin # center Y lenx = (xmax - xmin) + (margin * 2) leny = (ymax - ymin) + (margin * 2) # we need to make sure that the cutting polygon extends enough so it intersects the target # for that we need to add the cutting dia to gapsize in the corners that matter if gaps.lower() != 'none': if gaps == '8' or gaps in ['2LR', '2lr']: points = ( xmin - (gapsize + cut_dia), # botleft_x = X_MIN py - (gapsize / 2) + leny / 4, # botleft_y = Y_MIN xmax + (gapsize + cut_dia), # topright_x = X_MAX py + (gapsize / 2) + leny / 4 # topright_y = Y_MAX ) geom = self.subtract_poly_from_geo(geom, points) points = ( xmin - (gapsize + cut_dia), py - (gapsize / 2) - leny / 4, xmax + (gapsize + cut_dia), py + (gapsize / 2) - leny / 4 ) geom = self.subtract_poly_from_geo(geom, points) if gaps == '8' or gaps in ['2TB', '2tb']: points = ( px - (gapsize / 2) + lenx / 4, ymin - (gapsize + cut_dia), px + (gapsize / 2) + lenx / 4, ymax + (gapsize + cut_dia) ) geom = self.subtract_poly_from_geo(geom, points) points = ( px - (gapsize / 2) - lenx / 4, ymin - (gapsize + cut_dia), px + (gapsize / 2) - lenx / 4, ymax + (gapsize + cut_dia) ) geom = self.subtract_poly_from_geo(geom, points) if gaps == '4' or gaps in ['LR', 'lr']: points = ( xmin - (gapsize + cut_dia), py - (gapsize / 2), xmax + (gapsize + cut_dia), py + (gapsize / 2) ) geom = self.subtract_poly_from_geo(geom, points) if gaps == '4' or gaps in ['TB', 'tb']: points = ( px - (gapsize / 2), ymin - (gapsize + cut_dia), px + (gapsize / 2), ymax + (gapsize + cut_dia) ) geom = self.subtract_poly_from_geo(geom, points) try: # for g in geom: # proc_geometry.append(g) work_geom = geom.geoms if isinstance(geom, (MultiPolygon, MultiLineString)) else geom proc_geometry = [g for g in work_geom if not g.is_empty] except TypeError: # proc_geometry.append(geom) proc_geometry = [geom] return proc_geometry def on_drill_cut_click(self): margin = self.ui.drill_margin_entry.get_value() pitch = self.ui.drill_pitch_entry.get_value() drill_dia = self.ui.drill_dia_entry.get_value() name = self.ui.drillcut_object_combo.currentText() # Get source object. try: obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_freeform_cutout() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return "Could not retrieve object: %s" % name if obj is None: self.app.inform.emit('[ERROR_NOTCL] %s' % _("There is no object selected for Cutout.\nSelect one and try again.")) return cut_geo_solid = unary_union(obj.solid_geometry) drill_list = [] try: for geo in cut_geo_solid: if isinstance(geo, LineString): cut_geo = geo.parallel_offset(margin, side='left') elif isinstance(geo, Polygon): cut_geo = geo.buffer(margin).exterior else: self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("Could not add drills."))) return geo_length = cut_geo.length dist = 0 while dist <= geo_length: drill_list.append(cut_geo.interpolate(dist)) dist += pitch if dist < geo_length: drill_list.append(Point(list(cut_geo.coords)[-1])) except TypeError: if isinstance(cut_geo_solid, LineString): cut_geo = cut_geo_solid.parallel_offset(margin, side='left') elif isinstance(cut_geo_solid, Polygon): cut_geo = cut_geo_solid.buffer(margin).exterior else: self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("Could not add drills."))) return geo_length = cut_geo.length dist = 0 while dist <= geo_length: drill_list.append(cut_geo.interpolate(dist)) dist += pitch if dist < geo_length: drill_list.append(Point(list(cut_geo.coords)[-1])) if not drill_list: self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("Could not add drills."))) return tools = { 1: { "tooldia": drill_dia, "drills": drill_list, "slots": [], "solid_geometry": [] } } formatted_name = obj.obj_options['name'].rpartition('.')[0] if formatted_name == '': formatted_name = obj.obj_options['name'] outname = '%s_drillcut' % formatted_name def obj_init(obj_inst, app_inst): obj_inst.tools = deepcopy(tools) obj_inst.create_geometry() obj_inst.source_file = app_inst.f_handlers.export_excellon(obj_name=outname, local_use=obj_inst, filename=None, use_thread=False) with self.app.proc_container.new('%s...' % _("Working")): try: ret = self.app.app_obj.new_object("excellon", outname, obj_init, autoselected=False) except Exception as e: self.app.log.error("Error on Drill Cutting Excellon object creation: %s" % str(e)) return if ret != 'fail': self.app.inform.emit('[success] %s' % _("Done.")) def on_manual_gap_click(self): name = self.ui.man_object_combo.currentText() # Get source object. try: self.man_cutout_obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_manual_cutout() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return if self.man_cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Geometry object for manual cutout not found"), self.man_cutout_obj)) return self.app.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ...")) self.app.geo_editor.tool_shape.enabled = True self.manual_solid_geo = deepcopy(self.flatten(self.man_cutout_obj.solid_geometry)) self.cutting_dia = self.ui.dia.get_value() if 0 in {self.cutting_dia}: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) return if self.ui.gaptype_combo.get_value() == 2: # "mouse bytes" mb_dia = self.ui.mb_dia_entry.get_value() b_dia = (self.cutting_dia / 2.0) - (mb_dia / 2.0) # flaten manual geometry unified_man_geo = unary_union(self.manual_solid_geo) buff_man_geo = unified_man_geo.buffer(b_dia) if isinstance(buff_man_geo, MultiPolygon): int_list = [] for b_geo in buff_man_geo.geoms: int_list += b_geo.interiors elif isinstance(buff_man_geo, Polygon): int_list = buff_man_geo.interiors else: self.app.log.debug("Not supported geometry at the moment: %s" % type(buff_man_geo)) return self.mb_manual_solid_geo = self.flatten(int_list) self.cutting_gapsize = self.ui.gapsize.get_value() name = self.ui.man_object_combo.currentText() # Get Geometry source object to be used as target for Manual adding Gaps try: self.man_cutout_obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_manual_cutout() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent) self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot) else: self.app.plotcanvas.graph_event_disconnect(self.app.kp) self.app.plotcanvas.graph_event_disconnect(self.app.mp) self.app.plotcanvas.graph_event_disconnect(self.app.mr) self.app.plotcanvas.graph_event_disconnect(self.app.mm) self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press) self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) self.mouse_events_connected = True if self.ui.big_cursor_cb.get_value(): self.old_cursor_type = self.app.options["global_cursor_type"] self.app.on_cursor_type(val="big") self.app.options['global_selection_shape'] = False # disable the notebook until finished self.app.ui.notebook.setDisabled(True) def on_manual_cutout(self, click_pos): if self.man_cutout_obj is None: msg = '[ERROR_NOTCL] %s: %s' % (_("Geometry object for manual cutout not found"), self.man_cutout_obj) self.app.inform.emit(msg) return # use the snapped position as reference snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1]) cut_poly = self.cutting_geo(pos=(snapped_pos[0], snapped_pos[1])) gap_type = self.ui.gaptype_combo.get_value() gaps_solid_geo = None if gap_type == 1 and self.ui.thin_depth_entry.get_value() != 0: # "Thin gaps" gaps_solid_geo = self.intersect_geo(self.manual_solid_geo, cut_poly) if gap_type == 2: # "Mouse Bytes" rests_geo = self.intersect_geo(self.mb_manual_solid_geo, cut_poly) if isinstance(rests_geo, list): self.mb_manual_cuts += rests_geo else: self.mb_manual_cuts.append(rests_geo) # first subtract geometry for the total solid_geometry new_solid_geometry = self.subtract_geo(self.man_cutout_obj.solid_geometry, cut_poly) try: new_solid_geometry = linemerge(new_solid_geometry) except ValueError: pass self.man_cutout_obj.solid_geometry = new_solid_geometry # then do it on each tool in the manual cutout Geometry object try: self.man_cutout_obj.multigeo = True self.man_cutout_obj.tools[1]['solid_geometry'] = new_solid_geometry self.man_cutout_obj.tools[1]['data']['name'] = self.man_cutout_obj.obj_options['name'] + '_cutout' self.man_cutout_obj.tools[1]['data']['tools_mill_cutz'] = self.ui.cutz_entry.get_value() self.man_cutout_obj.tools[1]['data']['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() self.man_cutout_obj.tools[1]['data']['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() except KeyError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("No tool in the Geometry object.")) return dia = self.ui.dia.get_value() if gaps_solid_geo: if 99 not in self.man_cutout_obj.tools: self.man_cutout_obj.tools.update({ 99: self.cut_tool_dict }) self.man_cutout_obj.tools[99]['tooldia'] = str(dia) self.man_cutout_obj.tools[99]['solid_geometry'] = [gaps_solid_geo] self.man_cutout_obj.tools[99]['data']['name'] = self.man_cutout_obj.obj_options['name'] + '_cutout' self.man_cutout_obj.tools[99]['data']['tools_mill_cutz'] = self.ui.thin_depth_entry.get_value() self.man_cutout_obj.tools[99]['data']['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() self.man_cutout_obj.tools[99]['data']['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() self.man_cutout_obj.tools[99]['data']['override_color'] = "#29a3a3fa" else: self.man_cutout_obj.tools[99]['solid_geometry'].append(gaps_solid_geo) self.man_cutout_obj.plot(plot_tool=1) self.app.inform.emit('%s' % _("Added manual Bridge Gap. Left click to add another or right click to finish.")) self.app.should_we_save = True def on_manual_geo(self): name = self.ui.obj_combo.currentText() # Get source object. try: cutout_obj = self.app.collection.get_by_name(str(name)) except Exception as e: self.app.log.error("CutOut.on_manual_geo() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name)) return "Could not retrieve object: %s" % name if cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s' % _("There is no Gerber object selected for Cutout.\n" "Select one and try again.")) return if cutout_obj.kind != 'gerber': self.app.inform.emit('[ERROR_NOTCL] %s' % _("The selected object has to be of Gerber type.\n" "Select a Gerber file and try again.")) return dia = float(self.ui.dia.get_value()) try: kind = self.ui.obj_kind_combo.get_value() except ValueError: return margin = float(self.ui.margin.get_value()) convex_box = self.ui.convex_box_cb.get_value() def geo_init(geo_obj, app_obj): geo_union = unary_union(cutout_obj.solid_geometry) shape_type = self.ui.cutout_shape_cb.get_value() # True means rectangular shape if convex_box: geo = geo_union.convex_hull geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2)) elif kind == 'single': if isinstance(geo_union, Polygon) or \ (isinstance(geo_union, list) and len(geo_union) == 1) or \ (isinstance(geo_union, MultiPolygon) and len(geo_union.geoms) == 1): if dia >= 0: buff_geo = geo_union.buffer(margin + abs(dia / 2)).exterior else: buff_geo = geo_union.buffer(margin + abs(dia / 2)).interiors buff_geo = unary_union(buff_geo) if shape_type is False: geo_obj.solid_geometry = buff_geo else: geo_obj.solid_geometry = buff_geo.envelope elif isinstance(geo_union, MultiPolygon): if shape_type is False: buff_geo = geo_union.buffer(margin + abs(dia / 2)) geo_obj.solid_geometry = buff_geo else: x0, y0, x1, y1 = geo_union.bounds geo = box(x0, y0, x1, y1) buff_geo = geo.buffer(margin + abs(dia / 2)) geo_obj.solid_geometry = buff_geo else: app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % ( _("Geometry not supported"), type(geo_union))) return 'fail' else: geo = geo_union geo = geo.buffer(margin + abs(dia / 2)) if shape_type is False: if isinstance(geo, Polygon): geo_obj.solid_geometry = geo.exterior elif isinstance(geo, MultiPolygon): solid_geo = [] for poly in geo: solid_geo.append(poly.exterior) geo_obj.solid_geometry = deepcopy(solid_geo) else: if isinstance(geo, Polygon): geo_obj.solid_geometry = geo.envelope.exterior elif isinstance(geo, MultiPolygon): solid_geo = [] for poly in geo: solid_geo.append(poly.envelope.exterior) geo_obj.solid_geometry = deepcopy(unary_union(solid_geo).exterior) geo_obj.multigeo = True geo_obj.tools.update({ 1: self.cut_tool_dict }) geo_obj.tools[1]['tooldia'] = str(dia) geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry geo_obj.tools[1]['data']['name'] = outname geo_obj.tools[1]['data']['tools_mill_tooldia'] = str(dia) geo_obj.tools[1]['data']['tools_mill_cutz'] = self.ui.cutz_entry.get_value() geo_obj.tools[1]['data']['tools_mill_multidepth'] = self.ui.mpass_cb.get_value() geo_obj.tools[1]['data']['tools_mill_depthperpass'] = self.ui.maxdepth_entry.get_value() outname = cutout_obj.obj_options["name"] + "_cutout" self.app.app_obj.new_object('geometry', outname, geo_init, autoselected=False) def cutting_geo(self, pos): self.cutting_dia = float(self.ui.dia.get_value()) self.cutting_gapsize = float(self.ui.gapsize.get_value()) offset = self.cutting_dia / 2 + self.cutting_gapsize / 2 # cutting area definition orig_x = pos[0] orig_y = pos[1] xmin = orig_x - offset ymin = orig_y - offset xmax = orig_x + offset ymax = orig_y + offset cut_poly = box(xmin, ymin, xmax, ymax) return cut_poly # To be called after clicking on the plot. def on_mouse_click_release(self, event): if self.app.use_3d_engine: event_pos = event.pos # event_is_dragging = event.is_dragging right_button = 2 else: event_pos = (event.xdata, event.ydata) # event_is_dragging = self.app.plotcanvas.is_dragging right_button = 3 try: x = float(event_pos[0]) y = float(event_pos[1]) except TypeError: return event_pos = (x, y) # do paint single only for left mouse clicks if event.button == 1: self.app.inform.emit(_("Making manual bridge gap...")) pos = self.app.plotcanvas.translate_coords(event_pos) self.on_manual_cutout(click_pos=pos) # if RMB then we exit elif event.button == right_button and self.mouse_is_dragging is False: if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) else: self.app.plotcanvas.graph_event_disconnect(self.kp) self.app.plotcanvas.graph_event_disconnect(self.mm) self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) # Remove any previous utility shape self.app.geo_editor.tool_shape.clear(update=True) self.app.geo_editor.tool_shape.enabled = False # signal that the mouse events are disconnected from local methods self.mouse_events_connected = False if self.ui.big_cursor_cb.get_value(): # restore cursor self.app.on_cursor_type(val=self.old_cursor_type) # restore selection self.app.options['global_selection_shape'] = self.old_selection_state # rebuild the manual Geometry object self.man_cutout_obj.build_ui() # plot the final object self.man_cutout_obj.plot() # mouse bytes if self.ui.gaptype_combo.get_value() == 2: # "mouse bytes" with self.app.proc_container.new("Generating Excellon ..."): outname_exc = self.man_cutout_obj.obj_options["name"] + "_mouse_bites" self.app.collection.promise(outname_exc) def job_thread(app_obj): # list of Shapely Points to mark the drill points centers holes = [] mb_dia = self.ui.mb_dia_entry.get_value() mb_spacing = self.ui.mb_spacing_entry.get_value() for line in self.mb_manual_cuts: calc_len = 0 while calc_len <= line.length: holes.append(line.interpolate(calc_len)) calc_len += mb_dia + mb_spacing self.mb_manual_cuts[:] = [] def excellon_init(exc_obj, app_o): if not holes: return 'fail' tools = { 1: { "tooldia": mb_dia, "drills": holes, "solid_geometry": [] } } exc_obj.tools = tools exc_obj.create_geometry() exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.obj_options['name'], local_use=exc_obj, filename=None, use_thread=False) # calculate the bounds xmin, ymin, xmax, ymax = CutOut.recursive_bounds(exc_obj.solid_geometry) exc_obj.obj_options['xmin'] = xmin exc_obj.obj_options['ymin'] = ymin exc_obj.obj_options['xmax'] = xmax exc_obj.obj_options['ymax'] = ymax ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init, autoselected=False) if ret == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed.")) self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) self.app.ui.notebook.setDisabled(False) self.app.inform.emit('[success] %s' % _("Finished manual adding of gaps.")) def on_mouse_move(self, event): self.app.on_mouse_move_over_plot(event=event) if self.app.use_3d_engine: event_pos = event.pos event_is_dragging = event.is_dragging # right_button = 2 else: event_pos = (event.xdata, event.ydata) event_is_dragging = self.app.plotcanvas.is_dragging # right_button = 3 try: x = float(event_pos[0]) y = float(event_pos[1]) except TypeError: return event_pos = (x, y) pos = self.canvas.translate_coords(event_pos) event.xdata, event.ydata = pos[0], pos[1] if event_is_dragging is True: self.mouse_is_dragging = True else: self.mouse_is_dragging = False try: x = float(event.xdata) y = float(event.ydata) except TypeError: return if self.app.grid_status(): snap_x, snap_y = self.app.geo_editor.snap(x, y) else: snap_x, snap_y = x, y self.x_pos, self.y_pos = snap_x, snap_y # ################################################# # ### This section makes the cutting geo to ####### # ### rotate if it intersects the target geo ###### # ################################################# cut_geo = self.cutting_geo(pos=(snap_x, snap_y)) man_geo = self.man_cutout_obj.solid_geometry def get_angle(geo): line = cut_geo.intersection(geo) try: pt1_x = line.coords[0][0] pt1_y = line.coords[0][1] pt2_x = line.coords[1][0] pt2_y = line.coords[1][1] dx = pt1_x - pt2_x dy = pt1_y - pt2_y if dx == 0 or dy == 0: angle = 0 else: radian = math.atan(dx / dy) angle = radian * 180 / math.pi except Exception: angle = 0 return angle r_man_geo = man_geo.geoms if isinstance(man_geo, (MultiPolygon, MultiLineString)) else man_geo try: rot_angle = 0 for geo_el in r_man_geo: if isinstance(geo_el, Polygon): work_geo = geo_el.exterior rot_angle = get_angle(geo=work_geo) if cut_geo.intersects(work_geo) else 0 else: rot_angle = get_angle(geo=geo_el) if cut_geo.intersects(geo_el) else 0 if rot_angle != 0: break except TypeError: if isinstance(r_man_geo, Polygon): work_geo = r_man_geo.exterior rot_angle = get_angle(geo=work_geo) if cut_geo.intersects(work_geo) else 0 else: rot_angle = get_angle(geo=r_man_geo) if cut_geo.intersects(r_man_geo) else 0 # rotate only if there is an angle to rotate to if rot_angle != 0: cut_geo = rotate(cut_geo, -rot_angle) # Remove any previous utility shape self.app.geo_editor.tool_shape.clear(update=True) self.draw_utility_geometry(geo=cut_geo) def draw_utility_geometry(self, geo): self.app.geo_editor.tool_shape.add( shape=geo, color=(self.app.options["global_draw_color"]), update=False, layer=0, tolerance=None) self.app.geo_editor.tool_shape.redraw() def on_key_press(self, event): # events out of the self.app.collection view (it's about Project Tab) are of type int if type(event) is int: key = event # events from the GUI are of type QKeyEvent elif type(event) == QtGui.QKeyEvent: key = event.key() elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest key = event.key key = QtGui.QKeySequence(key) # check for modifiers key_string = key.toString().lower() if '+' in key_string: mod, __, key_text = key_string.rpartition('+') if mod.lower() == 'ctrl': # modifiers = QtCore.Qt.KeyboardModifier.ControlModifier pass elif mod.lower() == 'alt': # modifiers = QtCore.Qt.KeyboardModifier.AltModifier pass elif mod.lower() == 'shift': # modifiers = QtCore.Qt.KeyboardModifier.ShiftModifier pass else: # modifiers = QtCore.Qt.KeyboardModifier.NoModifier pass key = QtGui.QKeySequence(key_text) # events from Vispy are of type KeyEvent else: key = event.key # Escape = Deselect All if key == QtCore.Qt.Key.Key_Escape or key == 'Escape': if self.mouse_events_connected is True: self.mouse_events_connected = False if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) else: self.app.plotcanvas.graph_event_disconnect(self.kp) self.app.plotcanvas.graph_event_disconnect(self.mm) self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) if self.ui.big_cursor_cb.get_value(): # restore cursor self.app.on_cursor_type(val=self.old_cursor_type) # restore selection self.app.options['global_selection_shape'] = self.old_selection_state # Remove any previous utility shape self.app.geo_editor.tool_shape.clear(update=True) self.app.geo_editor.tool_shape.enabled = False # restore the notebook state self.app.ui.notebook.setDisabled(False) self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled.")) # Grid toggle if key == QtCore.Qt.Key.Key_G or key == 'G': self.app.ui.grid_snap_btn.trigger() # Jump to coords if key == QtCore.Qt.Key.Key_J or key == 'J': l_x, l_y = self.app.on_jump_to() self.app.geo_editor.tool_shape.clear(update=True) geo = self.cutting_geo(pos=(l_x, l_y)) self.draw_utility_geometry(geo=geo) def subtract_poly_from_geo(self, solid_geo, pts): """ Subtract polygon made from points from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths. :param solid_geo: Geometry from which to subtract. :param pts: a tuple of coordinates in format (x0, y0, x1, y1) :type pts: tuple x0: x coord for lower left vertex of the polygon. y0: y coord for lower left vertex of the polygon. x1: x coord for upper right vertex of the polygon. y1: y coord for upper right vertex of the polygon. :return: none """ x0 = pts[0] y0 = pts[1] x1 = pts[2] y1 = pts[3] points = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] # pathonly should be always True, otherwise polygons are not subtracted flat_geometry = CutOut.flatten(geometry=solid_geo) self.app.log.debug("%d paths" % len(flat_geometry)) polygon = Polygon(points) toolgeo = unary_union(polygon) diffs = [] for target in flat_geometry: if type(target) == LineString or type(target) == LinearRing: diffs.append(target.difference(toolgeo)) else: self.app.log.warning("Not implemented.") return unary_union(diffs) @staticmethod def flatten(geometry): """ Creates a list of non-iterable linear geometry objects. Polygons are expanded into its exterior and interiors. Results are placed in self.flat_geometry :param geometry: Shapely type or list or a list of lists of such. """ flat_geo = [] work_geo = geometry.geoms if isinstance(geometry, (MultiPolygon, MultiLineString)) else geometry try: for geo in work_geo: if geo: flat_geo += CutOut.flatten(geometry=geo) except TypeError: if isinstance(work_geo, Polygon) and not work_geo.is_empty: flat_geo.append(work_geo.exterior) CutOut.flatten(geometry=work_geo.interiors) elif not work_geo.is_empty: flat_geo.append(work_geo) return flat_geo @staticmethod def recursive_bounds(geometry): """ Return the bounds of the biggest bounding box in geometry, one that include all. :param geometry: a iterable object that holds geometry :return: Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax). """ # now it can get bounds for nested lists of objects def bounds_rec(obj): try: minx = Inf miny = Inf maxx = -Inf maxy = -Inf work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj for k in work_geo: if k.is_empty or not k.is_valid: continue minx_, miny_, maxx_, maxy_ = bounds_rec(k) minx = min(minx, minx_) miny = min(miny, miny_) maxx = max(maxx, maxx_) maxy = max(maxy, maxy_) return minx, miny, maxx, maxy except TypeError: # it's a Shapely object, return its bounds if obj: return obj.bounds return bounds_rec(geometry) def subtract_geo(self, target_geo, subtractor): """ Subtract subtractor polygon from the target_geo. This only operates on the paths in the target_geo, i.e. it converts polygons into paths. :param target_geo: geometry from which to subtract :param subtractor: a list of Points, a LinearRing or a Polygon that will be subtracted from target_geo :return: a unary_union of the resulting geometry """ if target_geo is None: target_geo = [] # flatten() takes care of possible empty geometry making sure that is filtered flat_geometry = CutOut.flatten(target_geo) self.app.log.debug("%d paths" % len(flat_geometry)) toolgeo = unary_union(subtractor) diffs = [] for target in flat_geometry: if isinstance(target, LineString) or isinstance(target, LinearRing) or isinstance(target, MultiLineString): d_geo = target.difference(toolgeo) if not d_geo.is_empty: diffs.append(d_geo) else: self.app.log.warning("Not implemented.") return unary_union(diffs) @staticmethod def intersect_geo(target_geo, second_geo): """ :param target_geo: :type target_geo: :param second_geo: :type second_geo: :return: :rtype: """ results = [] target_geo = flatten_shapely_geometry(target_geo) for geo in target_geo: if second_geo.intersects(geo): results.append(second_geo.intersection(geo)) return CutOut.flatten(results) def reset_fields(self): self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) class CutoutUI: pluginName = _("Cutout") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.layout = layout self.tools_frame = QtWidgets.QFrame() self.tools_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tools_frame) self.tools_box = QtWidgets.QVBoxLayout() self.tools_box.setContentsMargins(0, 0, 0, 0) self.tools_frame.setLayout(self.tools_box) self.title_box = QtWidgets.QHBoxLayout() self.tools_box.addLayout(self.title_box) # Title title_label = FCLabel("%s" % self.pluginName, size=16, bold=True) title_label.setToolTip( _("Create a Geometry object with toolpaths\n" "for cutting out the object from the surrounding material.") ) self.title_box.addWidget(title_label) # App Level label self.level = QtWidgets.QToolButton() self.level.setToolTip( _( "Beginner Mode - many parameters are hidden.\n" "Advanced Mode - full control.\n" "Permanent change is done in 'Preferences' menu." ) ) # self.level.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) self.level.setCheckable(True) self.title_box.addWidget(self.level) self.object_label = FCLabel('%s' % _("Source Object"), color='darkorange', bold=True) self.object_label.setToolTip('%s.' % _("Object to be cutout")) self.tools_box.addWidget(self.object_label) # ############################################################################################################# # Object Frame # ############################################################################################################# obj_frame = FCFrame() self.tools_box.addWidget(obj_frame) # Grid Layout obj_grid = GLay(v_spacing=5, h_spacing=3) obj_frame.setLayout(obj_grid) # Object kind self.kindlabel = FCLabel('%s:' % _('Kind')) self.kindlabel.setToolTip( _("Choice of what kind the object we want to cutout is.\n" "- Single: contain a single PCB Gerber outline object.\n" "- Panel: a panel PCB Gerber object, which is made\n" "out of many individual PCB outlines.") ) self.obj_kind_combo = RadioSet([ {"label": _("Single"), "value": "single"}, {"label": _("Panel"), "value": "panel"}, ]) obj_grid.addWidget(self.kindlabel, 2, 0) obj_grid.addWidget(self.obj_kind_combo, 2, 1) # Type of object to be cutout self.type_obj_radio = RadioSet([ {"label": _("Gerber"), "value": "grb"}, {"label": _("Geometry"), "value": "geo"}, ]) self.type_obj_combo_label = FCLabel('%s:' % _("Type")) self.type_obj_combo_label.setToolTip( _("Specify the type of object to be cutout.\n" "It can be of type: Gerber or Geometry.\n" "What is selected here will dictate the kind\n" "of objects that will populate the 'Object' combobox.") ) obj_grid.addWidget(self.type_obj_combo_label, 4, 0) obj_grid.addWidget(self.type_obj_radio, 4, 1) # Object to be cutout self.obj_combo = FCComboBox() self.obj_combo.setModel(self.app.collection) self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.obj_combo.is_last = False obj_grid.addWidget(self.obj_combo, 6, 0, 1, 2) self.tool_sel_label = FCLabel('%s' % _("Cutout Tool"), color='indigo', bold=True) self.tools_box.addWidget(self.tool_sel_label) # ############################################################################################################# # Tool Frame # ############################################################################################################# tool_frame = FCFrame() self.tools_box.addWidget(tool_frame) # Grid Layout tool_grid = GLay(v_spacing=5, h_spacing=3) tool_frame.setLayout(tool_grid) # Tool Diameter self.dia = FCDoubleSpinner(callback=self.confirmation_message) self.dia.set_precision(self.decimals) self.dia.set_range(-10000.0000, 10000.0000) self.dia_label = FCLabel('%s:' % _("Tool Dia")) self.dia_label.setToolTip( _("Diameter of the tool used to cutout\n" "the PCB shape out of the surrounding material.") ) tool_grid.addWidget(self.dia_label, 0, 0) tool_grid.addWidget(self.dia, 0, 1) hlay = QtWidgets.QHBoxLayout() # Search and Add new Tool self.add_newtool_button = FCButton(_('Search and Add')) self.add_newtool_button.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png')) self.add_newtool_button.setToolTip( _("Add a new tool to the Tool Table\n" "with the diameter specified above.\n" "This is done by a background search\n" "in the Tools Database. If nothing is found\n" "in the Tools DB then a default tool is added.") ) hlay.addWidget(self.add_newtool_button) # Pick from DB new Tool self.addtool_from_db_btn = FCButton(_('Pick from DB')) self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png')) self.addtool_from_db_btn.setToolTip( _("Add a new tool to the Tool Table\n" "from the Tools Database.\n" "Tools database administration in in:\n" "Menu: Options -> Tools Database") ) hlay.addWidget(self.addtool_from_db_btn) tool_grid.addLayout(hlay, 2, 0, 1, 2) # separator_line = QtWidgets.QFrame() # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # obj_grid.addWidget(separator_line, 18, 0, 1, 2) self.param_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True) self.tools_box.addWidget(self.param_label) # ############################################################################################################# # Tool Params Frame # ############################################################################################################# tool_par_frame = FCFrame() self.tools_box.addWidget(tool_par_frame) # Grid Layout param_grid = GLay(v_spacing=5, h_spacing=3) tool_par_frame.setLayout(param_grid) # Convex Shape # Surrounding convex box shape self.convex_box_label = FCLabel('%s:' % _("Convex Shape")) self.convex_box_label.setToolTip( _("Create a convex shape surrounding the entire PCB.\n" "Used only if the source object type is Gerber.") ) self.convex_box_cb = FCCheckBox() self.convex_box_cb.setToolTip( _("Create a convex shape surrounding the entire PCB.\n" "Used only if the source object type is Gerber.") ) param_grid.addWidget(self.convex_box_label, 0, 0) param_grid.addWidget(self.convex_box_cb, 0, 1) # Cut Z cutzlabel = FCLabel('%s:' % _('Cut Z')) cutzlabel.setToolTip( _("Cutting depth (negative)\n" "below the copper surface.") ) self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message) self.cutz_entry.set_precision(self.decimals) self.cutz_entry.setRange(-10000.0000, 10000.0000) self.cutz_entry.setSingleStep(0.1) param_grid.addWidget(cutzlabel, 2, 0) param_grid.addWidget(self.cutz_entry, 2, 1) # Multi-pass self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth")) self.mpass_cb.setToolTip( _("Use multiple passes to limit\n" "the cut depth in each pass. Will\n" "cut multiple times until Cut Z is\n" "reached.") ) self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message) self.maxdepth_entry.set_precision(self.decimals) self.maxdepth_entry.setRange(0, 10000.0000) self.maxdepth_entry.setSingleStep(0.1) self.maxdepth_entry.setToolTip(_("Depth of each pass (positive).")) param_grid.addWidget(self.mpass_cb, 4, 0) param_grid.addWidget(self.maxdepth_entry, 4, 1) self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry]) # Margin self.margin = FCDoubleSpinner(callback=self.confirmation_message) self.margin.set_range(-10000.0000, 10000.0000) self.margin.setSingleStep(0.1) self.margin.set_precision(self.decimals) self.margin_label = FCLabel('%s:' % _("Margin")) self.margin_label.setToolTip( _("Margin over bounds. A positive value here\n" "will make the cutout of the PCB further from\n" "the actual PCB border") ) param_grid.addWidget(self.margin_label, 6, 0) param_grid.addWidget(self.margin, 6, 1) self.gaps_label = FCLabel('%s' % _("Gaps"), color='green', bold=True) self.tools_box.addWidget(self.gaps_label) # ############################################################################################################# # Gaps Frame # ############################################################################################################# gaps_frame = FCFrame() self.tools_box.addWidget(gaps_frame) # Grid Layout gaps_grid = GLay(v_spacing=5, h_spacing=3) gaps_frame.setLayout(gaps_grid) # Gapsize self.gapsize_label = FCLabel('%s:' % _("Size")) self.gapsize_label.setToolTip( _("The size of the bridge gaps in the cutout\n" "used to keep the board connected to\n" "the surrounding material (the one \n" "from which the PCB is cutout).") ) self.gapsize = FCDoubleSpinner(callback=self.confirmation_message) self.gapsize.setRange(0.0000, 10000.0000) self.gapsize.set_precision(self.decimals) gaps_grid.addWidget(self.gapsize_label, 2, 0) gaps_grid.addWidget(self.gapsize, 2, 1) # Gap Type self.gaptype_label = FCLabel('%s:' % _("Type")) self.gaptype_label.setToolTip( _("The type of gap:\n" "- Bridge -> the cutout will be interrupted by bridges\n" "- Thin -> same as 'bridge' but it will be thinner by partially milling the gap\n" "- M-Bites -> 'Mouse Bites' - same as 'bridge' but covered with drill holes") ) # self.gaptype_combo = RadioSet( # [ # {'label': _('Bridge'), 'value': 'b'}, # {'label': _('Thin'), 'value': 'bt'}, # {'label': "M-Bites", 'value': 'mb'} # ], # compact=True # ) self.gaptype_combo = FCComboBox2() self.gaptype_combo.addItems([_('Bridge'), _('Thin'), _("Mouse Bytes")]) gaps_grid.addWidget(self.gaptype_label, 4, 0) gaps_grid.addWidget(self.gaptype_combo, 4, 1) # Thin gaps Depth self.thin_depth_label = FCLabel('%s:' % _("Depth")) self.thin_depth_label.setToolTip( _("The depth until the milling is done\n" "in order to thin the gaps.") ) self.thin_depth_entry = FCDoubleSpinner(callback=self.confirmation_message) self.thin_depth_entry.set_precision(self.decimals) self.thin_depth_entry.setRange(-10000.0000, 10000.0000) self.thin_depth_entry.setSingleStep(0.1) gaps_grid.addWidget(self.thin_depth_label, 6, 0) gaps_grid.addWidget(self.thin_depth_entry, 6, 1) # Mouse Bites Tool Diameter self.mb_dia_label = FCLabel('%s:' % _("Tool Dia")) self.mb_dia_label.setToolTip( _("The drill hole diameter when doing mouse bites.") ) self.mb_dia_entry = FCDoubleSpinner(callback=self.confirmation_message) self.mb_dia_entry.set_precision(self.decimals) self.mb_dia_entry.setRange(0, 10000.0000) gaps_grid.addWidget(self.mb_dia_label, 8, 0) gaps_grid.addWidget(self.mb_dia_entry, 8, 1) # Mouse Bites Holes Spacing self.mb_spacing_label = FCLabel('%s:' % _("Spacing")) self.mb_spacing_label.setToolTip( _("The spacing between drill holes when doing mouse bites.") ) self.mb_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message) self.mb_spacing_entry.set_precision(self.decimals) self.mb_spacing_entry.setRange(0, 10000.0000) gaps_grid.addWidget(self.mb_spacing_label, 10, 0) gaps_grid.addWidget(self.mb_spacing_entry, 10, 1) self.separator_line = QtWidgets.QFrame() self.separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) gaps_grid.addWidget(self.separator_line, 12, 0, 1, 2) # ############################################################################################################## # ######################################## Type of CUTOUT ###################################################### # ############################################################################################################## self.cutout_type_label = FCLabel('%s:' % _("Bridge")) self.cutout_type_label.setToolTip( _("Selection of the type of cutout.") ) self.cutout_type_radio = RadioSet([ {"label": _("Automatic"), "value": "a"}, {"label": _("Manual"), "value": "m"}, ]) gaps_grid.addWidget(self.cutout_type_label, 14, 0) gaps_grid.addWidget(self.cutout_type_radio, 14, 1) # Gaps # How gaps wil be rendered: # lr - left + right # tb - top + bottom # 4 - left + right +top + bottom # 2lr - 2*left + 2*right # 2tb - 2*top + 2*bottom # 8 - 2*left + 2*right +2*top + 2*bottom self.gaps_label = FCLabel('%s:' % _('Gaps')) self.gaps_label.setToolTip( _("Number of gaps used for the Automatic cutout.\n" "There can be maximum 8 bridges/gaps.\n" "The choices are:\n" "- None - no gaps\n" "- lr - left + right\n" "- tb - top + bottom\n" "- 4 - left + right +top + bottom\n" "- 2lr - 2*left + 2*right\n" "- 2tb - 2*top + 2*bottom\n" "- 8 - 2*left + 2*right +2*top + 2*bottom") ) # gaps_label.setMinimumWidth(60) self.gaps = FCComboBox() gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8'] for it in gaps_items: self.gaps.addItem(it) # self.gaps.setStyleSheet('background-color: rgb(255,255,255)') gaps_grid.addWidget(self.gaps_label, 16, 0) gaps_grid.addWidget(self.gaps, 16, 1) # Type of generated cutout: Rectangular or Any Form self.cutout_shape_label = FCLabel('%s:' % _("Shape")) self.cutout_shape_label.setToolTip( _("Checked: the cutout shape is rectangular.\n" "Unchecked: any-form cutout shape.") ) self.cutout_shape_cb = FCCheckBox('%s' % _("Any")) gaps_grid.addWidget(self.cutout_shape_label, 18, 0) gaps_grid.addWidget(self.cutout_shape_cb, 18, 1) # ############################################################################################################# # Manual Gaps Frame # ############################################################################################################# self.man_frame = QtWidgets.QFrame() self.man_frame.setContentsMargins(0, 0, 0, 0) gaps_grid.addWidget(self.man_frame, 20, 0, 1, 2) man_grid = GLay(v_spacing=5, h_spacing=3) man_grid.setContentsMargins(0, 0, 0, 0) self.man_frame.setLayout(man_grid) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) man_grid.addWidget(separator_line, 0, 0, 1, 2) # Big Cursor self.big_cursor_label = FCLabel('%s:' % _("Big cursor")) self.big_cursor_label.setToolTip( _("Use a big cursor when adding manual gaps.")) self.big_cursor_cb = FCCheckBox() man_grid.addWidget(self.big_cursor_label, 2, 0) man_grid.addWidget(self.big_cursor_cb, 2, 1) # Manual Geo Object self.man_object_combo = FCComboBox() self.man_object_combo.setModel(self.app.collection) self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) self.man_object_combo.is_last = True self.man_object_combo.obj_type = "Geometry" self.man_object_label = FCLabel('%s:' % _("Manual cutout Geometry")) self.man_object_label.setToolTip( _("Geometry object used to create the manual cutout.") ) # self.man_object_label.setMinimumWidth(60) man_grid.addWidget(self.man_object_label, 4, 0, 1, 2) man_grid.addWidget(self.man_object_combo, 6, 0, 1, 2) # ############################################################################################################# # Buttons # ############################################################################################################# man_hlay = QtWidgets.QHBoxLayout() self.tools_box.addLayout(man_hlay) # Generate a surrounding Geometry object Button self.man_geo_creation_btn = FCButton(_("Manual Geometry")) self.man_geo_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/rectangle32.png')) self.man_geo_creation_btn.setToolTip( _("Generate a Geometry to be used as cutout.") ) # self.man_geo_creation_btn.setStyleSheet(""" # QPushButton # { # font-weight: bold; # } # """) # Manual Add of Gaps Button self.man_gaps_creation_btn = QtWidgets.QToolButton() self.man_gaps_creation_btn.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.man_gaps_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus32.png')) self.man_gaps_creation_btn.setText(_("Gaps")) self.man_gaps_creation_btn.setToolTip( _("Add new gaps on the selected Geometry object\n" "by clicking mouse left button on the Geometry outline.") ) man_hlay.addWidget(self.man_geo_creation_btn) man_hlay.addWidget(self.man_gaps_creation_btn) # Generate Geometry Button self.generate_cutout_btn = FCButton(_("Generate Geometry"), bold=True) self.generate_cutout_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/irregular32.png')) self.generate_cutout_btn.setToolTip( _("Generate the cutout geometry.") ) self.tools_box.addWidget(self.generate_cutout_btn) # self.tool_param_separator_line = QtWidgets.QFrame() # self.tool_param_separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # self.tool_param_separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # obj_grid.addWidget(self.tool_param_separator_line, 60, 0, 1, 2) # obj_grid.addWidget(FCLabel(""), 62, 0, 1, 2) # Cut by Drilling Title self.title_drillcut_label = FCLabel('%s' % _("Cut by Drilling"), color='red', bold=True) self.title_drillcut_label.setToolTip(_("Create a series of drill holes following a geometry line.")) self.tools_box.addWidget(self.title_drillcut_label) # ############################################################################################################# # Cut by Drilling Frame # ############################################################################################################# self.drill_cut_frame = FCFrame() self.tools_box.addWidget(self.drill_cut_frame) # Grid Layout drill_cut_grid = GLay(v_spacing=5, h_spacing=3) self.drill_cut_frame.setLayout(drill_cut_grid) # Drilling Geo Object Label self.drillcut_object_lbl = FCLabel('%s:' % _("Geometry")) self.drillcut_object_lbl.setToolTip( _("Geometry object used to create the manual cutout.") ) drill_cut_grid.addWidget(self.drillcut_object_lbl, 0, 0, 1, 2) # Drilling Geo Object self.drillcut_object_combo = FCComboBox() self.drillcut_object_combo.setModel(self.app.collection) self.drillcut_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) self.drillcut_object_combo.is_last = False self.drillcut_object_combo.obj_type = "Geometry" drill_cut_grid.addWidget(self.drillcut_object_combo, 2, 0, 1, 2) # Drill Tool Diameter self.drill_dia_entry = FCDoubleSpinner(callback=self.confirmation_message) self.drill_dia_entry.set_precision(self.decimals) self.drill_dia_entry.set_range(0.0000, 10000.0000) self.drill_dia_label = FCLabel('%s:' % _("Drill Dia")) self.drill_dia_label.setToolTip( _("Diameter of the tool used to cutout\n" "the PCB by drilling.") ) drill_cut_grid.addWidget(self.drill_dia_label, 4, 0) drill_cut_grid.addWidget(self.drill_dia_entry, 4, 1) # Drill Tool Pitch self.drill_pitch_entry = FCDoubleSpinner(callback=self.confirmation_message) self.drill_pitch_entry.set_precision(self.decimals) self.drill_pitch_entry.set_range(0.0000, 10000.0000) self.drill_pitch_label = FCLabel('%s:' % _("Pitch")) self.drill_pitch_label.setToolTip( _("Distance between the center of\n" "two neighboring drill holes.") ) drill_cut_grid.addWidget(self.drill_pitch_label, 6, 0) drill_cut_grid.addWidget(self.drill_pitch_entry, 6, 1) # Drill Tool Margin self.drill_margin_entry = FCDoubleSpinner(callback=self.confirmation_message) self.drill_margin_entry.set_precision(self.decimals) self.drill_margin_entry.set_range(0.0000, 10000.0000) self.drill_margin_label = FCLabel('%s:' % _("Margin")) self.drill_margin_label.setToolTip( _("Margin over bounds. A positive value here\n" "will make the cutout of the PCB further from\n" "the actual PCB border") ) drill_cut_grid.addWidget(self.drill_margin_label, 8, 0) drill_cut_grid.addWidget(self.drill_margin_entry, 8, 1) GLay.set_common_column_size([obj_grid, tool_grid, param_grid, man_grid, drill_cut_grid, gaps_grid], 0) # Drill Cut Button self.drillcut_btn = FCButton(_("Cut by Drilling"), bold=True) self.drillcut_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/drill16.png')) self.drillcut_btn.setToolTip( _("Create a series of drill holes following a geometry line.") ) self.tools_box.addWidget(self.drillcut_btn) self.layout.addStretch() # ## Reset Tool self.reset_button = FCButton(_("Reset Tool"), bold=True) self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png')) self.reset_button.setToolTip( _("Will reset the tool parameters.") ) self.layout.addWidget(self.reset_button) self.gaptype_combo.currentIndexChanged.connect(self.on_gap_type_radio) # ############################ FINSIHED GUI ################################### # ############################################################################# def on_gap_type_radio(self, index): if index == 0: # Normal gap self.thin_depth_label.hide() self.thin_depth_entry.hide() self.mb_dia_label.hide() self.mb_dia_entry.hide() self.mb_spacing_label.hide() self.mb_spacing_entry.hide() elif index == 1: # "Thin gaps" self.thin_depth_label.show() self.thin_depth_entry.show() self.mb_dia_label.hide() self.mb_dia_entry.hide() self.mb_spacing_label.hide() self.mb_spacing_entry.hide() elif index == 2: # "Mouse Bytes" self.thin_depth_label.hide() self.thin_depth_entry.hide() self.mb_dia_label.show() self.mb_dia_entry.show() self.mb_spacing_label.show() self.mb_spacing_entry.show() def confirmation_message(self, accepted, minval, maxval): if accepted is False: self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), self.decimals, minval, self.decimals, maxval), False) else: self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def confirmation_message_int(self, accepted, minval, maxval): if accepted is False: self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % (_("Edited value is out of range"), minval, maxval), False) else: self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)