# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 10/25/2019 # # MIT Licence # # ########################################################## from appTool import * from appCommon.Common import LoudDict from appCommon.Common import GracefulException as grace from camlib import flatten_shapely_geometry import shapely.geometry.base as base fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class ToolCopperThieving(AppTool): work_finished = QtCore.pyqtSignal() def __init__(self, app): AppTool.__init__(self, app) self.app = app self.canvas = self.app.plotcanvas self.decimals = self.app.decimals self.units = self.app.app_units # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = ThievingUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() # Objects involved in Copper thieving self.grb_object = None self.ref_obj = None self.sel_rect = [] self.sm_object = None # store the flattened geometry here: self.flat_geometry = [] # Events ID self.mr = None self.mm = None # Mouse cursor positions self.mouse_is_dragging = False self.cursor_pos = (0, 0) self.first_click = False self.handlers_connected = False # Tool properties self.clearance_val = None self.margin_val = None self.geo_steps_per_circle = 128 # Thieving geometry storage self.thief_solid_geometry = [] # Robber bar geometry storage self.robber_geo = None self.robber_line = None self.rb_thickness = None def run(self, toggle=True): self.app.defaults.report_usage("ToolCopperThieving()") 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]) AppTool.run(self) self.set_tool_ui() self.app.ui.notebook.setTabText(2, _("Copper Thieving")) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+J', **kwargs) def connect_signals_at_init(self): # SIGNALS self.ui.ref_combo_type.currentIndexChanged.connect(self.on_ref_combo_type_change) self.ui.reference_combo.currentIndexChanged.connect(self.on_toggle_reference) self.ui.fill_type_combo.currentIndexChanged.connect(self.on_thieving_type) self.ui.fill_button.clicked.connect(self.on_add_copper_thieving_click) self.ui.rb_button.clicked.connect(self.on_add_robber_bar_click) self.ui.ppm_button.clicked.connect(self.on_add_ppm_click) self.ui.reset_button.clicked.connect(self.set_tool_ui) self.work_finished.connect(self.on_new_pattern_plating_object) def set_tool_ui(self): self.units = self.app.app_units self.geo_steps_per_circle = int(self.app.options["tools_copper_thieving_circle_steps"]) self.clear_ui(self.layout) self.ui = ThievingUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.ui.clearance_entry.set_value(float(self.app.options["tools_copper_thieving_clearance"])) self.ui.margin_entry.set_value(float(self.app.options["tools_copper_thieving_margin"])) self.ui.reference_combo.set_value(self.app.options["tools_copper_thieving_reference"]) self.ui.bbox_type_radio.set_value(self.app.options["tools_copper_thieving_box_type"]) self.ui.fill_type_combo.set_value(self.app.options["tools_copper_thieving_fill_type"]) self.ui.area_entry.set_value(self.app.options["tools_copper_thieving_area"]) self.ui.dot_dia_entry.set_value(self.app.options["tools_copper_thieving_dots_dia"]) self.ui.dot_spacing_entry.set_value(self.app.options["tools_copper_thieving_dots_spacing"]) self.ui.square_size_entry.set_value(self.app.options["tools_copper_thieving_squares_size"]) self.ui.squares_spacing_entry.set_value(self.app.options["tools_copper_thieving_squares_spacing"]) self.ui.line_size_entry.set_value(self.app.options["tools_copper_thieving_lines_size"]) self.ui.lines_spacing_entry.set_value(self.app.options["tools_copper_thieving_lines_spacing"]) self.ui.rb_margin_entry.set_value(self.app.options["tools_copper_thieving_rb_margin"]) self.ui.rb_thickness_entry.set_value(self.app.options["tools_copper_thieving_rb_thickness"]) self.ui.only_pads_cb.set_value(self.app.options["tools_copper_thieving_only_apds"]) self.ui.clearance_ppm_entry.set_value(self.app.options["tools_copper_thieving_mask_clearance"]) self.ui.ppm_choice_combo.set_value(self.app.options["tools_copper_thieving_geo_choice"]) # INIT SECTION self.handlers_connected = False self.robber_geo = None self.robber_line = None self.thief_solid_geometry = [] # SELECT THE CURRENT OBJECT obj = self.app.collection.get_active() if obj and obj.kind == 'gerber': obj_name = obj.obj_options['name'] self.ui.grb_object_combo.set_value(obj_name) if obj is None: self.ui.grb_object_combo.setCurrentIndex(0) def on_ref_combo_type_change(self): obj_type = self.ui.ref_combo_type.currentIndex() self.ui.ref_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.ref_combo.setCurrentIndex(0) self.ui.ref_combo.obj_type = { _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry" }[self.ui.ref_combo_type.get_value()] def on_toggle_reference(self): ref_val = self.ui.reference_combo.get_value() # if in ["itself", "area"] if ref_val == 0 or ref_val == 1: self.ui.ref_combo.hide() self.ui.ref_combo_type.hide() self.ui.ref_combo_type_label.hide() else: self.ui.ref_combo.show() self.ui.ref_combo_type.show() self.ui.ref_combo_type_label.show() if ref_val == 0: # "itself" self.ui.bbox_type_label.show() self.ui.bbox_type_radio.show() else: if self.ui.fill_type_combo.get_value() == 3: # 'line' self.ui.reference_combo.set_value(0) # 'itself' self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ...")) return self.ui.bbox_type_label.hide() self.ui.bbox_type_radio.hide() def on_thieving_type(self, new_index): if new_index == 0: # "solid" self.ui.dots_frame.hide() self.ui.squares_frame.hide() self.ui.lines_frame.hide() self.app.inform.emit(_("Solid fill selected.")) elif new_index == 1: # 'dot' self.ui.dots_frame.show() self.ui.squares_frame.hide() self.ui.lines_frame.hide() self.app.inform.emit(_("Dots grid fill selected.")) elif new_index == 2: # 'square' self.ui.dots_frame.hide() self.ui.squares_frame.show() self.ui.lines_frame.hide() self.app.inform.emit(_("Squares grid fill selected.")) else: # 'lines' ref_val = self.ui.reference_combo.get_value() if ref_val != 0: # 'itself' self.ui.reference_combo.set_value(0) # 'itself' self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ...")) self.ui.dots_frame.hide() self.ui.squares_frame.hide() self.ui.lines_frame.show() def on_add_robber_bar_click(self): rb_margin = self.ui.rb_margin_entry.get_value() self.rb_thickness = self.ui.rb_thickness_entry.get_value() # get the Gerber object on which the Robber bar will be inserted selection_index = self.ui.grb_object_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex()) try: self.grb_object = model_index.internalPointer().obj except Exception as e: self.app.log.error("ToolCopperThieving.on_add_robber_bar_click() --> %s" % str(e)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return try: outline_pol = self.grb_object.solid_geometry.envelope except (TypeError, AttributeError): outline_pol = MultiPolygon(self.grb_object.solid_geometry).envelope rb_distance = rb_margin + (self.rb_thickness / 2.0) self.robber_line = outline_pol.buffer(rb_distance).exterior self.robber_geo = self.robber_line.buffer(self.rb_thickness / 2.0) self.app.proc_container.update_view_text(' %s' % _("Append geometry")) new_apertures = deepcopy(self.grb_object.tools) aperture_found = None for ap_id, ap_val in self.grb_object.tools.items(): if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness: aperture_found = ap_id break if aperture_found: geo_elem = {'solid': self.robber_geo, 'follow': self.robber_line} new_apertures[aperture_found]['geometry'].append(deepcopy(geo_elem)) else: ap_keys = list(new_apertures.keys()) if ap_keys: new_apid = int(max(ap_keys)) + 1 else: new_apid = 10 new_apertures[new_apid] = { 'type': 'C', 'size': deepcopy(self.rb_thickness), 'geometry': [] } geo_elem = {'solid': self.robber_geo, 'follow': self.robber_line} new_apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) geo_obj = deepcopy(self.grb_object.solid_geometry) if isinstance(geo_obj, MultiPolygon): s_list = [] for pol in geo_obj.geoms: s_list.append(pol) s_list.append(deepcopy(self.robber_geo)) geo_obj = MultiPolygon(s_list) elif isinstance(geo_obj, list): geo_obj.append(deepcopy(self.robber_geo)) elif isinstance(geo_obj, Polygon): geo_obj = MultiPolygon([geo_obj, deepcopy(self.robber_geo)]) outname = '%s_%s' % (str(self.grb_object.obj_options['name']), 'robber') def initialize(grb_obj, app_obj): grb_obj.obj_options = LoudDict() for opt in self.grb_object.obj_options: if opt != 'name': grb_obj.obj_options[opt] = deepcopy(self.grb_object.obj_options[opt]) grb_obj.obj_options['name'] = outname grb_obj.multitool = False grb_obj.multigeo = False grb_obj.follow = deepcopy(self.grb_object.follow) grb_obj.tools = new_apertures grb_obj.solid_geometry = unary_union(geo_obj) grb_obj.follow_geometry = deepcopy(self.grb_object.follow_geometry) + [deepcopy(self.robber_line)] app_obj.proc_container.update_view_text(' %s' % _("Append source file")) # update the source file with the new geometry: grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj, use_thread=False) ret_val = self.app.app_obj.new_object('gerber', outname, initialize, plot=True) self.app.proc_container.update_view_text(' %s' % '') if ret_val == 'fail': self.app.call_source = "app" self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return self.on_exit() self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done.")) def on_add_copper_thieving_click(self): self.app.call_source = "copper_thieving_tool" self.clearance_val = self.ui.clearance_entry.get_value() self.margin_val = self.ui.margin_entry.get_value() reference_method = self.ui.reference_combo.get_value() # get the Gerber object on which the Copper thieving will be inserted selection_index = self.ui.grb_object_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex()) try: self.grb_object = model_index.internalPointer().obj except Exception as e: self.app.log.error("ToolCopperThieving.on_add_copper_thieving_click() --> %s" % str(e)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return if reference_method == 0: # 'itself' bound_obj_name = self.ui.grb_object_combo.currentText() # Get reference object. try: self.ref_obj = self.app.collection.get_by_name(bound_obj_name) except Exception as e: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e))) return "Could not retrieve object: %s" % self.grb_object.obj_options['name'] self.copper_thieving( thieving_obj=self.grb_object, c_val=self.clearance_val, margin=self.margin_val ) elif reference_method == 1: # 'area' self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area.")) self.connect_event_handlers() elif reference_method == 2: # 'box' bound_obj_name = self.ui.ref_combo.currentText() # Get reference object. try: self.ref_obj = self.app.collection.get_by_name(bound_obj_name) except Exception: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name)) return self.copper_thieving( thieving_obj=self.grb_object, ref_obj=self.ref_obj, c_val=self.clearance_val, margin=self.margin_val ) # To be called after clicking on the plot. def on_mouse_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 event_pos = self.app.plotcanvas.translate_coords(event_pos) # do clear area only for left mouse clicks if event.button == 1: if self.first_click is False: self.first_click = True self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area.")) self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos) if self.app.grid_status() is True: self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) else: self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish.")) self.app.delete_selection_shape() if self.app.grid_status() is True: curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) else: curr_pos = (event_pos[0], event_pos[1]) x0, y0 = self.cursor_pos[0], self.cursor_pos[1] x1, y1 = curr_pos[0], curr_pos[1] pt1 = (x0, y0) pt2 = (x1, y0) pt3 = (x1, y1) pt4 = (x0, y1) new_rectangle = Polygon([pt1, pt2, pt3, pt4]) self.sel_rect.append(new_rectangle) # add a temporary shape on canvas self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1)) self.first_click = False return elif event.button == right_button and self.mouse_is_dragging is False: self.first_click = False self.delete_tool_selection_shape() self.disconnect_event_handlers() if len(self.sel_rect) == 0: return self.sel_rect = unary_union(self.sel_rect) if not isinstance(self.sel_rect, Iterable): self.sel_rect = [self.sel_rect] self.copper_thieving( thieving_obj=self.grb_object, ref_obj=self.sel_rect, c_val=self.clearance_val, margin=self.margin_val ) # called on mouse move def on_mouse_move(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 curr_pos = self.app.plotcanvas.translate_coords(event_pos) # detect mouse dragging motion if event_is_dragging is True: self.mouse_is_dragging = True else: self.mouse_is_dragging = False # update the cursor position if self.app.grid_status() is True: # Update cursor curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1]) self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]), symbol='++', edge_color=self.app.plotcanvas.cursor_color, edge_width=self.app.options["global_cursor_width"], size=self.app.options["global_cursor_size"]) if self.cursor_pos is None: self.cursor_pos = (0, 0) self.app.dx = curr_pos[0] - float(self.cursor_pos[0]) self.app.dy = curr_pos[1] - float(self.cursor_pos[1]) # # update the positions on status bar # self.app.ui.position_label.setText(" X: %.4f   " # "Y: %.4f " % (curr_pos[0], curr_pos[1])) # self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " # "%.4f    " % (self.app.dx, self.app.dy)) self.app.ui.update_location_labels(self.app.dx, self.app.dy, curr_pos[0], curr_pos[1]) # units = self.app.app_units.lower() # self.app.plotcanvas.text_hud.text = \ # 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format( # self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units) self.app.plotcanvas.on_update_text_hud(self.app.dx, self.app.dy, curr_pos[0], curr_pos[1]) # draw the utility geometry if self.first_click: self.app.delete_selection_shape() self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]), coords=(curr_pos[0], curr_pos[1])) def copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True): """ :param thieving_obj: :param ref_obj: :param c_val: :param margin: :param run_threaded: :return: """ if run_threaded: self.app.proc_container.new('%s ...' % _("Thieving")) else: QtWidgets.QApplication.processEvents() self.app.proc_container.view.set_busy('%s ...' % _("Thieving")) # ##################################################################### # ####### Read the parameters ######################################### # ##################################################################### self.app.log.debug("Copper Thieving Tool started. Reading parameters.") self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters.")) ref_selected = self.ui.reference_combo.get_value() if c_val is None: c_val = float(self.app.options["tools_copper_thieving_clearance"]) if margin is None: margin = float(self.app.options["tools_copper_thieving_margin"]) min_area = self.ui.area_entry.get_value() fill_type = self.ui.fill_type_combo.get_value() dot_dia = self.ui.dot_dia_entry.get_value() dot_spacing = self.ui.dot_spacing_entry.get_value() square_size = self.ui.square_size_entry.get_value() square_spacing = self.ui.squares_spacing_entry.get_value() line_size = self.ui.line_size_entry.get_value() line_spacing = self.ui.lines_spacing_entry.get_value() # make sure that the source object solid geometry is an Iterable if not isinstance(self.grb_object.solid_geometry, Iterable): self.grb_object.solid_geometry = [self.grb_object.solid_geometry] def job_thread_thieving(tool_obj): # ######################################################################################################### # Prepare isolation polygon. This will create the clearance over the Gerber features # ######################################################################################################### self.app.log.debug("Copper Thieving Tool. Preparing isolation polygons.") tool_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons.")) # variables to display the percentage of work done try: geo_len = len(tool_obj.grb_object.solid_geometry) except TypeError: geo_len = 1 old_disp_number = 0 pol_nr = 0 # ######################################################################################################### # apply the clearance value to the geometry # ######################################################################################################### clearance_geometry = [] try: for pol in tool_obj.grb_object.solid_geometry: if tool_obj.app.abort_flag: # graceful abort requested by the user raise grace clearance_geometry.append( pol.buffer(c_val, int(int(tool_obj.geo_steps_per_circle) / 4)) ) pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) if old_disp_number < disp_number <= 100: msg = ' %s ... %d%%' % (_("Thieving"), int(disp_number)) tool_obj.app.proc_container.update_view_text(msg) old_disp_number = disp_number except TypeError: # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a # MultiPolygon (not an iterable) clearance_geometry.append( tool_obj.grb_object.solid_geometry.buffer(c_val, int(int(tool_obj.geo_steps_per_circle) / 4)) ) tool_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering")) clearance_geometry = unary_union(clearance_geometry) # ######################################################################################################### # Prepare the area to fill with copper. # ######################################################################################################### tool_obj.app.log.debug("Copper Thieving Tool. Preparing areas to fill with copper.") tool_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper.")) try: if ref_obj is None or ref_selected == 0: # 'itself' working_obj = thieving_obj else: working_obj = ref_obj except Exception as e: tool_obj.app.log.error("ToolCopperThieving.copper_thieving() --> %s" % str(e)) return 'fail' tool_obj.app.proc_container.update_view_text(' %s' % _("Working...")) # ######################################################################################################### # generate the bounding box geometry # ######################################################################################################### if ref_selected == 0: # 'itself' geo_n = deepcopy(working_obj.solid_geometry) try: if tool_obj.ui.bbox_type_radio.get_value() == 'min': if isinstance(geo_n, MultiPolygon): env_obj = geo_n.convex_hull elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \ (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon): env_obj = unary_union(geo_n) else: env_obj = unary_union(geo_n) env_obj = env_obj.convex_hull bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) else: if isinstance(geo_n, Polygon): bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) elif isinstance(geo_n, list): geo_n = MultiPolygon(geo_n) x0, y0, x1, y1 = geo_n.bounds geo = box(x0, y0, x1, y1) bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) elif isinstance(geo_n, MultiPolygon): x0, y0, x1, y1 = geo_n.bounds geo = box(x0, y0, x1, y1) bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) else: tool_obj.app.inform.emit( '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for"), type(geo_n)) ) return 'fail' except Exception as e: tool_obj.app.log.error("ToolCopperFIll.copper_thieving() 'itself' --> %s" % str(e)) tool_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available.")) return 'fail' elif ref_selected == 1: # 'area' geo_buff_list = [] try: for poly in working_obj: if tool_obj.app.abort_flag: # graceful abort requested by the user raise grace geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)) except TypeError: geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)) bounding_box = MultiPolygon(geo_buff_list) else: # ref_selected == 'box' geo_n = working_obj.solid_geometry if working_obj.kind == 'geometry': geo_n = flatten_shapely_geometry(geo_n) geo_buff_list = [] for poly in geo_n: if tool_obj.app.abort_flag: # graceful abort requested by the user raise grace geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)) bounding_box = unary_union(geo_buff_list) elif working_obj.kind == 'gerber': geo_n = unary_union(geo_n).convex_hull bounding_box = unary_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n) bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) else: tool_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported.")) return 'fail' self.app.log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.") tool_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering.")) # ######################################################################################################### # Generate solid filling geometry. Effectively it's a NEGATIVE of the source object # ######################################################################################################### tool_obj.thief_solid_geometry = bounding_box.difference(clearance_geometry) temp_geo = [] try: for s_geo in tool_obj.thief_solid_geometry: if s_geo.area >= min_area: temp_geo.append(s_geo) except TypeError: if tool_obj.thief_solid_geometry.area >= min_area: temp_geo.append(tool_obj.thief_solid_geometry) tool_obj.thief_solid_geometry = temp_geo # ######################################################################################################### # apply the 'margin' to the bounding box geometry # ######################################################################################################### try: bounding_box = thieving_obj.solid_geometry.envelope.buffer( distance=margin, join_style=base.JOIN_STYLE.mitre ) except AttributeError: bounding_box = MultiPolygon(thieving_obj.solid_geometry).envelope.buffer( distance=margin, join_style=base.JOIN_STYLE.mitre ) x0, y0, x1, y1 = bounding_box.bounds # ######################################################################################################### # add Thieving geometry # ######################################################################################################### tool_obj.app.proc_container.update_view_text(' %s' % _("Create geometry")) if fill_type == 1 or fill_type == 2: # 'dot' or 'square' # build the MultiPolygon of dots/squares that will fill the entire bounding box thieving_list = [] if fill_type == 1: # 'dot' radius = dot_dia / 2.0 new_x = x0 + radius new_y = y0 + radius while new_x <= x1 - radius: while new_y <= y1 - radius: dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64) thieving_list.append(dot_geo) new_y += dot_dia + dot_spacing new_x += dot_dia + dot_spacing new_y = y0 + radius else: # 'square' h_size = square_size / 2.0 new_x = x0 + h_size new_y = y0 + h_size while new_x <= x1 - h_size: while new_y <= y1 - h_size: a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds square_geo = box(a, b, c, d) thieving_list.append(square_geo) new_y += square_size + square_spacing new_x += square_size + square_spacing new_y = y0 + h_size thieving_box_geo = MultiPolygon(thieving_list) dx = bounding_box.centroid.x - thieving_box_geo.centroid.x dy = bounding_box.centroid.y - thieving_box_geo.centroid.y thieving_box_geo = translate(thieving_box_geo, xoff=dx, yoff=dy) thieving_box_geo = flatten_shapely_geometry(thieving_box_geo) thieving_geo = [] for dot_geo in thieving_box_geo: for geo_t in tool_obj.thief_solid_geometry: if dot_geo.within(geo_t): thieving_geo.append(dot_geo) tool_obj.thief_solid_geometry = thieving_geo if fill_type == 3: # 'line' half_thick_line = line_size / 2.0 # create a thick polygon-line that surrounds the copper features outline_geometry = [] try: for pol in tool_obj.grb_object.solid_geometry: if tool_obj.app.abort_flag: # graceful abort requested by the user raise grace outline_geometry.append( pol.buffer(c_val+half_thick_line, int(int(tool_obj.geo_steps_per_circle) / 4)) ) pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) if old_disp_number < disp_number <= 100: msg = ' %s ... %d%%' % (_("Buffering"), int(disp_number)) tool_obj.app.proc_container.update_view_text(msg) old_disp_number = disp_number except TypeError: # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a # MultiPolygon (not an iterable) outline_geometry.append( tool_obj.grb_object.solid_geometry.buffer( c_val+half_thick_line, int(int(tool_obj.geo_steps_per_circle) / 4) ) ) tool_obj.app.proc_container.update_view_text(' %s' % _("Buffering")) outline_geometry = unary_union(outline_geometry) outline_line = [] try: for geo_o in outline_geometry: outline_line.append( geo_o.exterior.buffer( half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4) ) ) except TypeError: outline_line.append( outline_geometry.exterior.buffer( half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4) ) ) outline_geometry = unary_union(outline_line) # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line) box_outline_geo_exterior = box_outline_geo.exterior box_outline_geometry = box_outline_geo_exterior.buffer( half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4) ) bx0, by0, bx1, by1 = box_outline_geo.bounds thieving_lines_geo = [] new_x = bx0 new_y = by0 while new_x <= x1 - half_thick_line: line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer( half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4) ) thieving_lines_geo.append(line_geo) new_x += line_size + line_spacing while new_y <= y1 - half_thick_line: line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer( half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4) ) thieving_lines_geo.append(line_geo) new_y += line_size + line_spacing # merge everything together diff_lines_geo = [] for line_poly in thieving_lines_geo: rest_line = line_poly.difference(clearance_geometry) diff_lines_geo.append(rest_line) tool_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo]) tool_obj.thief_solid_geometry = tool_obj.flat_geometry tool_obj.app.proc_container.update_view_text(' %s' % _("Append geometry")) # create a list of the source geometry geo_list = deepcopy(tool_obj.grb_object.solid_geometry) if isinstance(tool_obj.grb_object.solid_geometry, MultiPolygon): geo_list = list(geo_list.geoms) # create a new dictionary to hold the source object apertures allowing us to tamper with without altering # the original source object's apertures new_apertures = deepcopy(tool_obj.grb_object.tools) if 0 not in new_apertures: new_apertures[0] = { 'type': 'REG', 'size': 0.0, 'geometry': [] } # add the thieving geometry in the 0 aperture of the new_apertures dict try: for poly in tool_obj.thief_solid_geometry: # append to the new solid geometry geo_list.append(poly) # append into the 0 aperture geo_elem = {'solid': poly, 'follow': poly.exterior} new_apertures[0]['geometry'].append(deepcopy(geo_elem)) except TypeError: # append to the new solid geometry geo_list.append(tool_obj.thief_solid_geometry) # append into the 0 aperture geo_elem = {'solid': tool_obj.thief_solid_geometry, 'follow': tool_obj.thief_solid_geometry.exterior} new_apertures[0]['geometry'].append(deepcopy(geo_elem)) # prepare also the solid_geometry for the new object having the thieving geometry new_solid_geo = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001) outname = '%s_%s' % (str(self.grb_object.obj_options['name']), 'thief') def initialize(grb_obj, app_obj): grb_obj.obj_options = LoudDict() for opt in self.grb_object.obj_options: if opt != 'name': grb_obj.obj_options[opt] = deepcopy(self.grb_object.obj_options[opt]) grb_obj.obj_options['name'] = outname grb_obj.multitool = False grb_obj.multigeo = False grb_obj.follow = deepcopy(self.grb_object.follow) grb_obj.tools = new_apertures grb_obj.solid_geometry = deepcopy(new_solid_geo) grb_obj.follow_geometry = deepcopy(self.grb_object.follow_geometry) app_obj.proc_container.update_view_text(' %s' % _("Append source file")) grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj, use_thread=False) ret_val = self.app.app_obj.new_object('gerber', outname, initialize, plot=True, autoselected=False) tool_obj.app.proc_container.update_view_text(' %s' % '') if ret_val == 'fail': self.app.call_source = "app" self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return tool_obj.on_exit() tool_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done.")) if run_threaded: self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]}) else: job_thread_thieving(self) def on_add_ppm_click(self): run_threaded = True if run_threaded: self.app.proc_container.new('%s ...' % _("P-Plating Mask")) else: QtWidgets.QApplication.processEvents() self.app.proc_container.view.set_busy('%s ...' % _("P-Plating Mask")) if run_threaded: self.app.worker_task.emit({'fcn': self.on_new_pattern_plating_object, 'params': []}) else: self.on_new_pattern_plating_object() def on_new_pattern_plating_object(self): ppm_clearance = self.ui.clearance_ppm_entry.get_value() geo_choice = self.ui.ppm_choice_combo.get_value() rb_thickness = self.ui.rb_thickness_entry.get_value() only_pads = self.ui.only_pads_cb.get_value() # get the Gerber object on which the Copper thieving will be inserted selection_index = self.ui.sm_object_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.ui.sm_object_combo.rootModelIndex()) try: self.sm_object = model_index.internalPointer().obj except Exception as e: self.app.log.error("ToolCopperThieving.on_add_ppm_click() --> %s" % str(e)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return self.app.proc_container.update_view_text(' %s' % _("Append PP-M geometry")) geo_list = [] if only_pads is False: geo_list = deepcopy(self.sm_object.solid_geometry) if isinstance(geo_list, MultiPolygon): geo_list = list(geo_list.geoms) else: for apid in self.sm_object.tools: for k in self.sm_object.tools[apid]: if k == 'geometry': for elem in self.sm_object.tools[apid]['geometry']: if 'follow' in elem and isinstance(elem['follow'], Point): if 'solid' in elem: geo_list.append(elem['solid']) # create a copy of the source apertures so we can manipulate them without altering the source object new_apertures = deepcopy(self.sm_object.tools) # if the clearance is not zero apply it to the original soldermask geometry too if ppm_clearance != 0: temp_geo_list = [] for geo in geo_list: temp_geo_list.append(geo.buffer(ppm_clearance)) geo_list = temp_geo_list # squash former geometry in apertures for ap_id in new_apertures: for k in new_apertures[ap_id]: if k == 'geometry': new_apertures[ap_id]['geometry'] = [] # then add a buffered geometry for ap_id in new_apertures: if 'geometry' in self.sm_object.tools[ap_id]: new_geo_list = [] for geo_el in self.sm_object.tools[ap_id]['geometry']: new_el = { 'solid': geo_el['solid'].buffer(ppm_clearance) if 'solid' in geo_el else [], 'follow': geo_el['follow'] if 'follow' in geo_el else [], 'clear': geo_el['clear'] if 'clear' in geo_el else [] } new_geo_list.append(deepcopy(new_el)) new_apertures[ap_id]['geometry'] = deepcopy(new_geo_list) # calculate its own plated area (from the solder mask object) plated_area = 0.0 for geo in geo_list: plated_area += geo.area thieving_solid_geo = deepcopy(self.thief_solid_geometry) robber_solid_geo = deepcopy(self.robber_geo) robber_line = deepcopy(self.robber_line) # store here the chosen follow geometry new_follow_geo = deepcopy(self.sm_object.follow_geometry) # if we have copper thieving geometry, add it if thieving_solid_geo and geo_choice in [0, 1]: # ['b', 't'] # add to the total the thieving geometry area, if chosen for geo in thieving_solid_geo: plated_area += geo.area if 0 not in new_apertures: new_apertures[0] = { 'type': 'REG', 'size': 0.0, 'geometry': [] } try: for poly in thieving_solid_geo: poly_b = poly.buffer(ppm_clearance) # append to the new solid geometry geo_list.append(poly_b) # append into the 0 aperture geo_elem = { 'solid': poly_b, 'follow': poly_b.exterior } new_apertures[0]['geometry'].append(deepcopy(geo_elem)) except TypeError: # append to the new solid geometry assert isinstance(thieving_solid_geo, Polygon) geo_list.append(thieving_solid_geo.buffer(ppm_clearance)) # append into the 0 aperture geo_elem = { 'solid': thieving_solid_geo.buffer(ppm_clearance), 'follow': thieving_solid_geo.buffer(ppm_clearance).exterior } new_apertures[0]['geometry'].append(deepcopy(geo_elem)) # if we have robber bar geometry, add it if robber_solid_geo and geo_choice in [0, 2]: # 'b', 'r' # add to the total the robber bar geometry are, if chose plated_area += robber_solid_geo.area # add to the follow_geomery new_follow_geo.append(robber_line) aperture_found = None for ap_id, ap_val in new_apertures.items(): if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness + ppm_clearance: aperture_found = ap_id break if aperture_found: geo_elem = {'solid': robber_solid_geo, 'follow': robber_line} new_apertures[aperture_found]['geometry'].append(deepcopy(geo_elem)) else: ap_keys = list(new_apertures.keys()) max_apid = int(max(ap_keys)) if ap_keys and max_apid != 0: new_apid = max_apid + 1 else: new_apid = 10 new_apertures[new_apid] = { 'type': 'C', 'size': rb_thickness + ppm_clearance, 'geometry': [] } geo_elem = { 'solid': robber_solid_geo.buffer(ppm_clearance), 'follow': deepcopy(robber_line) } new_apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) geo_list.append(robber_solid_geo.buffer(ppm_clearance)) # and then set the total plated area value to the GUI element # the area is in mm2 when using Metric units, make it in cm2 for Metric units if self.units.lower() == 'mm': plated_area /= 100 self.ui.plated_area_entry.set_value(plated_area) new_solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001) def obj_init(grb_obj, app_obj): grb_obj.obj_options = LoudDict() for opt in self.sm_object.obj_options: if opt != 'name': grb_obj.obj_options[opt] = deepcopy(self.sm_object.obj_options[opt]) grb_obj.obj_options['name'] = outname grb_obj.multitool = False grb_obj.source_file = [] grb_obj.multigeo = False grb_obj.follow = False grb_obj.follow_geometry = deepcopy(new_follow_geo) grb_obj.tools = deepcopy(new_apertures) grb_obj.solid_geometry = deepcopy(new_solid_geometry) app_obj.proc_container.update_view_text(' %s' % _("Append source file")) # update the source file with the new geometry: grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj, use_thread=False) app_obj.proc_container.update_view_text(' %s' % '') # Object name obj_name, separatpr, obj_extension = self.sm_object.obj_options['name'].rpartition('.') outname = '%s_%s.%s' % (obj_name, 'plating_mask', obj_extension) ret_val = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False) if ret_val == 'fail': self.app.call_source = "app" self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return # Register recent file self.app.file_opened.emit("gerber", outname) self.on_exit() self.app.inform.emit('[success] %s' % _("Generating Pattern Plating Mask done.")) def replot(self, obj, run_thread=True): def worker_task(): with self.app.proc_container.new('%s...' % _("Plotting")): obj.plot() self.app.app_obj.object_plotted.emit(obj) if run_thread: self.app.worker_task.emit({'fcn': worker_task, 'params': []}) else: worker_task() def on_exit(self, obj=None): # plot the objects if obj: try: for ob in obj: self.replot(obj=ob) except (AttributeError, TypeError): self.replot(obj=obj) except Exception: return # reset the variables self.sel_rect = [] # Events ID self.mr = None self.mm = None # Mouse cursor positions self.mouse_is_dragging = False self.cursor_pos = (0, 0) self.first_click = False # if True it means we exited from tool in the middle of area adding therefore disconnect the events if self.handlers_connected is True: self.app.delete_selection_shape() self.disconnect_event_handlers() self.app.call_source = "app" self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit.")) def connect_event_handlers(self): if self.handlers_connected is False: if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) else: self.app.plotcanvas.graph_event_disconnect(self.app.mp) self.app.plotcanvas.graph_event_disconnect(self.app.mm) self.app.plotcanvas.graph_event_disconnect(self.app.mr) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) self.handlers_connected = True def disconnect_event_handlers(self): if self.handlers_connected is True: if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) else: self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.plotcanvas.graph_event_disconnect(self.mm) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) self.handlers_connected = False def flatten(self, geometry): """ Creates a list of non-iterable linear geometry objects. :param geometry: Shapely type or list or list of list of such. Results are placed in self.flat_geometry """ # ## If iterable, expand recursively. try: for geo in geometry: if geo is not None: self.flatten(geometry=geo) # ## Not iterable, do the actual indexing and add. except TypeError: self.flat_geometry.append(geometry) return self.flat_geometry class ThievingUI: pluginName = _("Copper Thieving") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.units = self.app.app_units self.layout = layout # ## Title title_label = FCLabel("%s" % self.pluginName) title_label.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; } """) self.layout.addWidget(title_label) 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) # ############################################################################################################# # Source Object Frame # ############################################################################################################# self.grbobj_label = FCLabel('%s' % (self.app.theme_safe_color('darkorange'), _("Source Object"))) self.grbobj_label.setToolTip(_("Gerber Object to which will be added a copper thieving.")) self.tools_box.addWidget(self.grbobj_label) # ## Grid Layout i_grid_lay = GLay(v_spacing=5, h_spacing=3) self.tools_box.addLayout(i_grid_lay) self.grb_object_combo = FCComboBox() self.grb_object_combo.setModel(self.app.collection) self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.grb_object_combo.is_last = False self.grb_object_combo.obj_type = 'Gerber' i_grid_lay.addWidget(self.grb_object_combo, 0, 0, 1, 2) # ############################################################################################################# # Thieving Parameters Frame # ############################################################################################################# self.copper_fill_label = FCLabel('%s %s' % (_('Thieving'), _("Parameters"))) self.copper_fill_label.setToolTip(_("Parameters used for this tool.")) self.tools_box.addWidget(self.copper_fill_label) tp_frame = FCFrame() self.tools_box.addWidget(tp_frame) grid_lay = GLay(v_spacing=5, h_spacing=3) tp_frame.setLayout(grid_lay) # CLEARANCE # self.clearance_label = FCLabel('%s:' % _("Clearance")) self.clearance_label.setToolTip( _("This set the distance between the copper thieving components\n" "(the polygon fill may be split in multiple polygons)\n" "and the copper traces in the Gerber file.") ) self.clearance_entry = FCDoubleSpinner(callback=self.confirmation_message) self.clearance_entry.set_range(0.00001, 10000.0000) self.clearance_entry.set_precision(self.decimals) self.clearance_entry.setSingleStep(0.1) grid_lay.addWidget(self.clearance_label, 2, 0) grid_lay.addWidget(self.clearance_entry, 2, 1) # MARGIN # self.margin_label = FCLabel('%s:' % _("Margin")) self.margin_label.setToolTip( _("Bounding box margin.") ) self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message) self.margin_entry.set_range(0.0, 10000.0000) self.margin_entry.set_precision(self.decimals) self.margin_entry.setSingleStep(0.1) grid_lay.addWidget(self.margin_label, 4, 0) grid_lay.addWidget(self.margin_entry, 4, 1) # Area # area_hlay = QtWidgets.QHBoxLayout() self.area_label = FCLabel('%s:' % _("Area")) self.area_label.setToolTip( _("Thieving areas with area less then this value will not be added.") ) self.area_entry = FCDoubleSpinner(callback=self.confirmation_message) self.area_entry.set_range(0.0, 10000.0000) self.area_entry.set_precision(self.decimals) self.area_entry.setSingleStep(0.1) self.area_entry.setSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Preferred) if self.units.upper() == 'MM': units_area_label = FCLabel('%s2' % _("mm")) else: units_area_label = FCLabel('%s2' % _("in")) area_hlay.addWidget(self.area_entry) area_hlay.addWidget(units_area_label) grid_lay.addWidget(self.area_label, 6, 0) grid_lay.addLayout(area_hlay, 6, 1) # Reference # self.reference_label = FCLabel(_("Reference:")) self.reference_label.setToolTip( _("- 'Itself' - the copper thieving extent is based on the object extent.\n" "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n" "- 'Reference Object' - will do copper thieving within the area specified by another object.") ) self.reference_combo = FCComboBox2() self.reference_combo.addItems([_('Itself'), _("Area Selection"), _("Reference Object")]) grid_lay.addWidget(self.reference_label, 8, 0) grid_lay.addWidget(self.reference_combo, 8, 1) self.ref_combo_type_label = FCLabel('%s:' % _("Type")) self.ref_combo_type_label.setToolTip( _("The type of FlatCAM object to be used as copper thieving reference.\n" "It can be Gerber, Excellon or Geometry.") ) self.ref_combo_type = FCComboBox() self.ref_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")]) grid_lay.addWidget(self.ref_combo_type_label, 10, 0) grid_lay.addWidget(self.ref_combo_type, 10, 1) # Object Reference self.ref_combo = FCComboBox() self.ref_combo.setModel(self.app.collection) self.ref_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ref_combo.is_last = True self.ref_combo.obj_type = { _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry" }[self.ref_combo_type.get_value()] grid_lay.addWidget(self.ref_combo, 12, 0, 1, 2) self.ref_combo.hide() self.ref_combo_type.hide() self.ref_combo_type_label.hide() # Bounding Box Type # self.bbox_type_label = FCLabel('%s:' % _("Box Type")) self.bbox_type_label.setToolTip( _("- 'Rectangular' - the bounding box will be of rectangular shape.\n" "- 'Minimal' - the bounding box will be the convex hull shape.") ) self.bbox_type_radio = RadioSet([ {'label': _('Rectangular'), 'value': 'rect'}, {"label": _("Minimal"), "value": "min"} ], compact=True) grid_lay.addWidget(self.bbox_type_label, 14, 0) grid_lay.addWidget(self.bbox_type_radio, 14, 1) self.bbox_type_label.hide() self.bbox_type_radio.hide() separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) grid_lay.addWidget(separator_line, 16, 0, 1, 2) # Fill Type self.fill_type_label = FCLabel('%s:' % _("Fill")) self.fill_type_label.setToolTip( _("- 'Solid' - copper thieving will be a solid polygon.\n" "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n" "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n" "- 'Lines Grid' - the empty area will be filled with a pattern of lines.") ) self.fill_type_combo = FCComboBox2() self.fill_type_combo.addItems([_('Solid'), _("Dots Grid"), _("Squares Grid"), _("Lines Grid")]) grid_lay.addWidget(self.fill_type_label, 18, 0) grid_lay.addWidget(self.fill_type_combo, 18, 1) # ############################################################################################################# # DOTS FRAME # ############################################################################################################# self.dots_frame = QtWidgets.QFrame() self.dots_frame.setContentsMargins(0, 0, 0, 0) grid_lay.addWidget(self.dots_frame, 20, 0, 1, 2) dots_grid = GLay(v_spacing=5, h_spacing=3) dots_grid.setContentsMargins(0, 0, 0, 0) self.dots_frame.setLayout(dots_grid) self.dots_frame.hide() separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) dots_grid.addWidget(separator_line, 0, 0, 1, 2) self.dots_label = FCLabel('%s:' % _("Dots Grid Parameters")) dots_grid.addWidget(self.dots_label, 2, 0, 1, 2) # Dot diameter # self.dotdia_label = FCLabel('%s:' % _("Dia")) self.dotdia_label.setToolTip( _("Dot diameter in Dots Grid.") ) self.dot_dia_entry = FCDoubleSpinner(callback=self.confirmation_message) self.dot_dia_entry.set_range(0.0, 10000.0000) self.dot_dia_entry.set_precision(self.decimals) self.dot_dia_entry.setSingleStep(0.1) dots_grid.addWidget(self.dotdia_label, 4, 0) dots_grid.addWidget(self.dot_dia_entry, 4, 1) # Dot spacing # self.dotspacing_label = FCLabel('%s:' % _("Spacing")) self.dotspacing_label.setToolTip( _("Distance between each two dots in Dots Grid.") ) self.dot_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message) self.dot_spacing_entry.set_range(0.0, 10000.0000) self.dot_spacing_entry.set_precision(self.decimals) self.dot_spacing_entry.setSingleStep(0.1) dots_grid.addWidget(self.dotspacing_label, 6, 0) dots_grid.addWidget(self.dot_spacing_entry, 6, 1) # ############################################################################################################# # SQUARES FRAME # ############################################################################################################# self.squares_frame = QtWidgets.QFrame() self.squares_frame.setContentsMargins(0, 0, 0, 0) grid_lay.addWidget(self.squares_frame, 22, 0, 1, 2) squares_grid = GLay(v_spacing=5, h_spacing=3) squares_grid.setContentsMargins(0, 0, 0, 0) self.squares_frame.setLayout(squares_grid) self.squares_frame.hide() separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) squares_grid.addWidget(separator_line, 0, 0, 1, 2) self.squares_label = FCLabel('%s:' % _("Squares Grid Parameters")) squares_grid.addWidget(self.squares_label, 2, 0, 1, 2) # Square Size # self.square_size_label = FCLabel('%s:' % _("Size")) self.square_size_label.setToolTip( _("Square side size in Squares Grid.") ) self.square_size_entry = FCDoubleSpinner(callback=self.confirmation_message) self.square_size_entry.set_range(0.0, 10000.0000) self.square_size_entry.set_precision(self.decimals) self.square_size_entry.setSingleStep(0.1) squares_grid.addWidget(self.square_size_label, 4, 0) squares_grid.addWidget(self.square_size_entry, 4, 1) # Squares spacing # self.squares_spacing_label = FCLabel('%s:' % _("Spacing")) self.squares_spacing_label.setToolTip( _("Distance between each two squares in Squares Grid.") ) self.squares_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message) self.squares_spacing_entry.set_range(0.0, 10000.0000) self.squares_spacing_entry.set_precision(self.decimals) self.squares_spacing_entry.setSingleStep(0.1) squares_grid.addWidget(self.squares_spacing_label, 6, 0) squares_grid.addWidget(self.squares_spacing_entry, 6, 1) # ############################################################################################################# # LINES FRAME # ############################################################################################################# self.lines_frame = QtWidgets.QFrame() self.lines_frame.setContentsMargins(0, 0, 0, 0) grid_lay.addWidget(self.lines_frame, 24, 0, 1, 2) lines_grid = GLay(v_spacing=5, h_spacing=3) lines_grid.setContentsMargins(0, 0, 0, 0) self.lines_frame.setLayout(lines_grid) self.lines_frame.hide() separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) lines_grid.addWidget(separator_line, 0, 0, 1, 2) self.lines_label = FCLabel('%s:' % _("Lines Grid Parameters")) lines_grid.addWidget(self.lines_label, 2, 0, 1, 2) # Line Size # self.line_size_label = FCLabel('%s:' % _("Size")) self.line_size_label.setToolTip( _("Line thickness size in Lines Grid.") ) self.line_size_entry = FCDoubleSpinner(callback=self.confirmation_message) self.line_size_entry.set_range(0.0, 10000.0000) self.line_size_entry.set_precision(self.decimals) self.line_size_entry.setSingleStep(0.1) lines_grid.addWidget(self.line_size_label, 4, 0) lines_grid.addWidget(self.line_size_entry, 4, 1) # Lines spacing # self.lines_spacing_label = FCLabel('%s:' % _("Spacing")) self.lines_spacing_label.setToolTip( _("Distance between each two lines in Lines Grid.") ) self.lines_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message) self.lines_spacing_entry.set_range(0.0, 10000.0000) self.lines_spacing_entry.set_precision(self.decimals) self.lines_spacing_entry.setSingleStep(0.1) lines_grid.addWidget(self.lines_spacing_label, 6, 0) lines_grid.addWidget(self.lines_spacing_entry, 6, 1) # ############################################################################################################# # ## Insert Copper Thieving BUTTON # ############################################################################################################# self.fill_button = QtWidgets.QPushButton(_("Insert Copper thieving")) self.fill_button.setIcon(QtGui.QIcon(self.app.resource_location + '/copperfill32.png')) self.fill_button.setToolTip( _("Will add a polygon (may be split in multiple parts)\n" "that will surround the actual Gerber traces at a certain distance.") ) self.fill_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.fill_button) # ############################################################################################################# # ## Robber Bar Parameters # ############################################################################################################# self.robber_bar_label = FCLabel('%s' % _('Robber Bar Parameters')) self.robber_bar_label.setToolTip( _("Parameters used for the robber bar.\n" "Robber bar = copper border to help in pattern hole plating.") ) self.tools_box.addWidget(self.robber_bar_label) rob_frame = FCFrame() self.tools_box.addWidget(rob_frame) # ## Grid Layout grid_lay_1 = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) rob_frame.setLayout(grid_lay_1) # separator_line_1 = QtWidgets.QFrame() # separator_line_1.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line_1.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3) # ROBBER BAR MARGIN # self.rb_margin_label = FCLabel('%s:' % _("Margin")) self.rb_margin_label.setToolTip( _("Bounding box margin for robber bar.") ) self.rb_margin_entry = FCDoubleSpinner(callback=self.confirmation_message) self.rb_margin_entry.set_range(-10000.0000, 10000.0000) self.rb_margin_entry.set_precision(self.decimals) self.rb_margin_entry.setSingleStep(0.1) grid_lay_1.addWidget(self.rb_margin_label, 4, 0) grid_lay_1.addWidget(self.rb_margin_entry, 4, 1, 1, 2) # THICKNESS # self.rb_thickness_label = FCLabel('%s:' % _("Thickness")) self.rb_thickness_label.setToolTip( _("The robber bar thickness.") ) self.rb_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message) self.rb_thickness_entry.set_range(0.0000, 10000.0000) self.rb_thickness_entry.set_precision(self.decimals) self.rb_thickness_entry.setSingleStep(0.1) grid_lay_1.addWidget(self.rb_thickness_label, 6, 0) grid_lay_1.addWidget(self.rb_thickness_entry, 6, 1, 1, 2) # ############################################################################################################# # ## Insert Robber Bar # ############################################################################################################# self.rb_button = QtWidgets.QPushButton(_("Insert Robber Bar")) self.rb_button.setIcon(QtGui.QIcon(self.app.resource_location + '/robber32.png')) self.rb_button.setToolTip( _("Will add a polygon with a defined thickness\n" "that will surround the actual Gerber object\n" "at a certain distance.\n" "Required when doing holes pattern plating.") ) self.rb_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.rb_button) # ############################################################################################################# # Pattern plating Frame # ############################################################################################################# self.patern_mask_label = FCLabel('%s' % _('Pattern Plating Mask')) self.patern_mask_label.setToolTip( _("Generate a mask for pattern plating.") ) self.tools_box.addWidget(self.patern_mask_label) self.sm_obj_label = FCLabel('%s' % (self.app.theme_safe_color('darkorange'), _("Source Object"))) self.sm_obj_label.setToolTip( _("Gerber Object with the soldermask.\n" "It will be used as a base for\n" "the pattern plating mask.") ) self.sm_object_combo = FCComboBox() self.sm_object_combo.setModel(self.app.collection) self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.sm_object_combo.is_last = True self.sm_object_combo.obj_type = 'Gerber' self.tools_box.addWidget(self.sm_obj_label) self.tools_box.addWidget(self.sm_object_combo) pp_frame = FCFrame() self.tools_box.addWidget(pp_frame) # ## Grid Layout grid_lay_2 = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) pp_frame.setLayout(grid_lay_2) # Only Pads self.only_pads_cb = FCCheckBox(_("Only Pads")) self.only_pads_cb.setToolTip( _("Select only pads in case the selected object is a copper Gerber.") ) grid_lay_2.addWidget(self.only_pads_cb, 0, 0, 1, 3) # Openings CLEARANCE # self.clearance_ppm_label = FCLabel('%s:' % _("Clearance")) self.clearance_ppm_label.setToolTip( _("The distance between the possible copper thieving elements\n" "and/or robber bar and the actual openings in the mask.") ) self.clearance_ppm_entry = FCDoubleSpinner(callback=self.confirmation_message) self.clearance_ppm_entry.set_range(-10000.0000, 10000.0000) self.clearance_ppm_entry.set_precision(self.decimals) self.clearance_ppm_entry.setSingleStep(0.1) grid_lay_2.addWidget(self.clearance_ppm_label, 2, 0) grid_lay_2.addWidget(self.clearance_ppm_entry, 2, 1, 1, 2) # Plated area self.plated_area_label = FCLabel('%s:' % _("Plated area")) self.plated_area_label.setToolTip( _("The area to be plated by pattern plating.\n" "Basically is made from the openings in the plating mask.\n\n" "<> - the calculated area is actually a bit larger\n" "due of the fact that the soldermask openings are by design\n" "a bit larger than the copper pads, and this area is\n" "calculated from the soldermask openings.") ) self.plated_area_entry = FCEntry() self.plated_area_entry.setReadOnly(True) if self.units.upper() == 'MM': self.units_area_label = FCLabel('%s2' % _("cm")) else: self.units_area_label = FCLabel('%s2' % _("in")) grid_lay_2.addWidget(self.plated_area_label, 4, 0) grid_lay_2.addWidget(self.plated_area_entry, 4, 1) grid_lay_2.addWidget(self.units_area_label, 4, 2) # Include geometry self.ppm_choice_label = FCLabel('%s:' % _("Add")) self.ppm_choice_label.setToolTip( _("Choose which additional geometry to include, if available.") ) # self.ppm_choice_combo = RadioSet([ # {"label": _("Both"), "value": "b"}, # {'label': _('Thieving'), 'value': 't'}, # {"label": _("Robber bar"), "value": "r"}, # {"label": _("None"), "value": "n"} # ], orientation='vertical', compact=True) self.ppm_choice_combo = FCComboBox2() self.ppm_choice_combo.addItems([_("Both"), _('Thieving'), _("Robber bar"), _("None")]) grid_lay_2.addWidget(self.ppm_choice_label, 6, 0) grid_lay_2.addWidget(self.ppm_choice_combo, 6, 1, 1, 2) GLay.set_common_column_size( [i_grid_lay, grid_lay, grid_lay_1, grid_lay_2, dots_grid, squares_grid, lines_grid], 0) # ############################################################################################################# # ## Pattern Plating Mask Button # ############################################################################################################# self.ppm_button = QtWidgets.QPushButton(_("Generate pattern plating mask")) self.ppm_button.setIcon(QtGui.QIcon(self.app.resource_location + '/pattern32.png')) self.ppm_button.setToolTip( _("Will add to the soldermask gerber geometry\n" "the geometries of the copper thieving and/or\n" "the robber bar if those were generated.") ) self.ppm_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.ppm_button) self.layout.addStretch(1) # ## Reset Tool self.reset_button = QtWidgets.QPushButton(_("Reset Tool")) self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png')) self.reset_button.setToolTip( _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.layout.addWidget(self.reset_button) # #################################### FINSIHED GUI ########################### # ############################################################################# 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)