# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 10/25/2019 # # MIT Licence # # ########################################################## from PyQt5 import QtWidgets, QtCore, QtGui from camlib import grace from appTool import AppTool from appGUI.GUIElements import FCDoubleSpinner, RadioSet, FCEntry, FCComboBox, FCLabel import shapely.geometry.base as base from shapely.ops import unary_union from shapely.geometry import Polygon, MultiPolygon, Point, LineString from shapely.geometry import box as box import shapely.affinity as affinity import logging from copy import deepcopy import numpy as np try: from collections import Iterable except ImportError: from collections.abc import Iterable import gettext import appTranslation as fcTranslate import builtins 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.defaults['units'] # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = ThievingUI(layout=self.layout, app=self.app) self.toolName = self.ui.toolName # 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 # SIGNALS self.ui.ref_combo_type.currentIndexChanged.connect(self.on_ref_combo_type_change) self.ui.reference_radio.group_toggle_fn = self.on_toggle_reference self.ui.fill_type_radio.activated_custom.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 run(self, toggle=True): self.app.defaults.report_usage("ToolCopperThieving()") if toggle: # if the splitter is hidden, display it, else hide it but only if the current widget is the same if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) else: try: if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName: # if tab is populated with the tool but it does not have the focus, focus on it if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab: # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab) else: 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 Tool")) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+J', **kwargs) def set_tool_ui(self): self.units = self.app.defaults['units'] self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"]) self.ui.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"])) self.ui.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"])) self.ui.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"]) self.ui.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"]) self.ui.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"]) self.ui.area_entry.set_value(self.app.defaults["tools_copper_thieving_area"]) self.ui.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"]) self.ui.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"]) self.ui.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"]) self.ui.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"]) self.ui.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"]) self.ui.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"]) self.ui.rb_margin_entry.set_value(self.app.defaults["tools_copper_thieving_rb_margin"]) self.ui.rb_thickness_entry.set_value(self.app.defaults["tools_copper_thieving_rb_thickness"]) self.ui.clearance_ppm_entry.set_value(self.app.defaults["tools_copper_thieving_mask_clearance"]) self.ui.ppm_choice_radio.set_value(self.app.defaults["tools_copper_thieving_geo_choice"]) # INIT SECTION self.handlers_connected = False self.robber_geo = None self.robber_line = None self.thief_solid_geometry = [] 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): if self.ui.reference_radio.get_value() == "itself" or self.ui.reference_radio.get_value() == "area": self.ui.ref_combo.hide() self.ui.ref_combo_label.hide() self.ui.ref_combo_type.hide() self.ui.ref_combo_type_label.hide() else: self.ui.ref_combo.show() self.ui.ref_combo_label.show() self.ui.ref_combo_type.show() self.ui.ref_combo_type_label.show() if self.ui.reference_radio.get_value() == "itself": self.ui.bbox_type_label.show() self.ui.bbox_type_radio.show() else: if self.ui.fill_type_radio.get_value() == 'line': self.ui.reference_radio.set_value('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, choice): if choice == 'solid': self.ui.dots_frame.hide() self.ui.squares_frame.hide() self.ui.lines_frame.hide() self.app.inform.emit(_("Solid fill selected.")) elif choice == '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 choice == '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: if self.ui.reference_radio.get_value() != 'itself': self.ui.reference_radio.set_value('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: log.debug("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.apertures) aperture_found = None for ap_id, ap_val in self.grb_object.apertures.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 = str(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.options['name']), 'robber') def initialize(grb_obj, app_obj): grb_obj.options = {} for opt in self.grb_object.options: if opt != 'name': grb_obj.options[opt] = deepcopy(self.grb_object.options[opt]) grb_obj.options['name'] = outname grb_obj.multitool = False grb_obj.multigeo = False grb_obj.follow = deepcopy(self.grb_object.follow) grb_obj.apertures = 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_radio.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: log.debug("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 == '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.obj_name self.copper_thieving( thieving_obj=self.grb_object, c_val=self.clearance_val, margin=self.margin_val ) elif reference_method == 'area': self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area.")) self.connect_event_handlers() elif reference_method == '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.is_legacy is False: 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.is_legacy is False: 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.cursor_color_3D, edge_width=self.app.defaults["global_cursor_width"], size=self.app.defaults["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)) units = self.app.defaults["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) # 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 ######################################### # ##################################################################### log.debug("Copper Thieving Tool started. Reading parameters.") self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters.")) ref_selected = self.ui.reference_radio.get_value() if c_val is None: c_val = float(self.app.defaults["tools_copper_thieving_clearance"]) if margin is None: margin = float(self.app.defaults["tools_copper_thieving_margin"]) min_area = self.ui.area_entry.get_value() fill_type = self.ui.fill_type_radio.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 # ######################################################################################################### 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. # ######################################################################################################### 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_obj == 'itself': working_obj = thieving_obj else: working_obj = ref_obj except Exception as e: log.debug("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 == '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: log.debug("ToolCopperFIll.copper_thieving() 'itself' --> %s" % str(e)) tool_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available.")) return 'fail' elif ref_selected == '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': try: __ = iter(geo_n) except Exception as e: log.debug("ToolCopperFIll.copper_thieving() 'box' --> %s" % str(e)) geo_n = [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' 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 == 'dot' or fill_type == 'square': # build the MultiPolygon of dots/squares that will fill the entire bounding box thieving_list = [] if fill_type == '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: 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 = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy) try: _it = iter(thieving_box_geo) except TypeError: thieving_box_geo = [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 == '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.apertures) 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.new_solid_geometry, 'follow': tool_obj.new_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.options['name']), 'thief') def initialize(grb_obj, app_obj): grb_obj.options = {} for opt in self.grb_object.options: if opt != 'name': grb_obj.options[opt] = deepcopy(self.grb_object.options[opt]) grb_obj.options['name'] = outname grb_obj.multitool = False grb_obj.multigeo = False grb_obj.follow = deepcopy(self.grb_object.follow) grb_obj.apertures = 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) 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_radio.get_value() rb_thickness = self.rb_thickness # 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: log.debug("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 = deepcopy(self.sm_object.solid_geometry) if isinstance(geo_list, MultiPolygon): geo_list = list(geo_list.geoms) # create a copy of the source apertures so we can manipulate them without altering the source object new_apertures = deepcopy(self.sm_object.apertures) # if the clearance is negative 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.apertures[ap_id]: new_geo_list = [] for geo_el in self.sm_object.apertures[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 ['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 ['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 = str(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 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.options = {} for opt in self.sm_object.options: if opt != 'name': grb_obj.options[opt] = deepcopy(self.sm_object.options[opt]) grb_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.apertures = 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.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.is_legacy is False: 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.is_legacy is False: 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: toolName = _("Copper Thieving Tool") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.units = self.app.defaults['units'] self.layout = layout # ## Title title_label = FCLabel("%s" % self.toolName) title_label.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; } """) self.layout.addWidget(title_label) self.layout.addWidget(FCLabel("")) # ## Grid Layout i_grid_lay = QtWidgets.QGridLayout() self.layout.addLayout(i_grid_lay) i_grid_lay.setColumnStretch(0, 0) i_grid_lay.setColumnStretch(1, 1) 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 = True self.grb_object_combo.obj_type = 'Gerber' self.grbobj_label = FCLabel("%s:" % _("GERBER")) self.grbobj_label.setToolTip( _("Gerber Object to which will be added a copper thieving.") ) i_grid_lay.addWidget(self.grbobj_label, 0, 0) i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) i_grid_lay.addWidget(separator_line, 2, 0, 1, 2) # ## Grid Layout grid_lay = QtWidgets.QGridLayout() self.layout.addLayout(grid_lay) grid_lay.setColumnStretch(0, 0) grid_lay.setColumnStretch(1, 1) self.copper_fill_label = FCLabel('%s' % _('Parameters')) self.copper_fill_label.setToolTip( _("Parameters used for this tool.") ) grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2) # 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.MinimumExpanding, QtWidgets.QSizePolicy.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_radio = RadioSet([ {'label': _('Itself'), 'value': 'itself'}, {"label": _("Area Selection"), "value": "area"}, {'label': _("Reference Object"), 'value': 'box'} ], orientation='vertical', stretch=False) 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.") ) grid_lay.addWidget(self.reference_label, 8, 0) grid_lay.addWidget(self.reference_radio, 8, 1) self.ref_combo_type_label = FCLabel('%s:' % _("Ref. 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) self.ref_combo_label = FCLabel('%s:' % _("Ref. Object")) self.ref_combo_label.setToolTip( _("The FlatCAM object to be used as non copper clearing 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_label, 12, 0) grid_lay.addWidget(self.ref_combo, 12, 1) self.ref_combo.hide() self.ref_combo_label.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"} ], stretch=False) 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.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid_lay.addWidget(separator_line, 16, 0, 1, 2) # Fill Type self.fill_type_radio = RadioSet([ {'label': _('Solid'), 'value': 'solid'}, {"label": _("Dots Grid"), "value": "dot"}, {"label": _("Squares Grid"), "value": "square"}, {"label": _("Lines Grid"), "value": "line"} ], orientation='vertical', stretch=False) self.fill_type_label = FCLabel(_("Fill Type:")) 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.") ) grid_lay.addWidget(self.fill_type_label, 18, 0) grid_lay.addWidget(self.fill_type_radio, 18, 1) # DOTS FRAME self.dots_frame = QtWidgets.QFrame() self.dots_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.dots_frame) dots_grid = QtWidgets.QGridLayout() dots_grid.setColumnStretch(0, 0) dots_grid.setColumnStretch(1, 1) dots_grid.setContentsMargins(0, 0, 0, 0) self.dots_frame.setLayout(dots_grid) self.dots_frame.hide() self.dots_label = FCLabel('%s:' % _("Dots Grid Parameters")) dots_grid.addWidget(self.dots_label, 0, 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, 1, 0) dots_grid.addWidget(self.dot_dia_entry, 1, 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, 2, 0) dots_grid.addWidget(self.dot_spacing_entry, 2, 1) # SQUARES FRAME self.squares_frame = QtWidgets.QFrame() self.squares_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.squares_frame) squares_grid = QtWidgets.QGridLayout() squares_grid.setColumnStretch(0, 0) squares_grid.setColumnStretch(1, 1) squares_grid.setContentsMargins(0, 0, 0, 0) self.squares_frame.setLayout(squares_grid) self.squares_frame.hide() self.squares_label = FCLabel('%s:' % _("Squares Grid Parameters")) squares_grid.addWidget(self.squares_label, 0, 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, 1, 0) squares_grid.addWidget(self.square_size_entry, 1, 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, 2, 0) squares_grid.addWidget(self.squares_spacing_entry, 2, 1) # LINES FRAME self.lines_frame = QtWidgets.QFrame() self.lines_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.lines_frame) lines_grid = QtWidgets.QGridLayout() lines_grid.setColumnStretch(0, 0) lines_grid.setColumnStretch(1, 1) lines_grid.setContentsMargins(0, 0, 0, 0) self.lines_frame.setLayout(lines_grid) self.lines_frame.hide() self.lines_label = FCLabel('%s:' % _("Lines Grid Parameters")) lines_grid.addWidget(self.lines_label, 0, 0, 1, 2) # Square 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, 1, 0) lines_grid.addWidget(self.line_size_entry, 1, 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, 2, 0) lines_grid.addWidget(self.lines_spacing_entry, 2, 1) # ## Insert Copper Thieving 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.layout.addWidget(self.fill_button) # ## Grid Layout grid_lay_1 = QtWidgets.QGridLayout() self.layout.addLayout(grid_lay_1) grid_lay_1.setColumnStretch(0, 0) grid_lay_1.setColumnStretch(1, 1) grid_lay_1.setColumnStretch(2, 0) separator_line_1 = QtWidgets.QFrame() separator_line_1.setFrameShape(QtWidgets.QFrame.HLine) separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken) grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3) grid_lay_1.addWidget(FCLabel('')) 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.") ) grid_lay_1.addWidget(self.robber_bar_label, 2, 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; } """) grid_lay_1.addWidget(self.rb_button, 8, 0, 1, 3) separator_line_2 = QtWidgets.QFrame() separator_line_2.setFrameShape(QtWidgets.QFrame.HLine) separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken) grid_lay_1.addWidget(separator_line_2, 10, 0, 1, 3) self.patern_mask_label = FCLabel('%s' % _('Pattern Plating Mask')) self.patern_mask_label.setToolTip( _("Generate a mask for pattern plating.") ) grid_lay_1.addWidget(self.patern_mask_label, 12, 0, 1, 3) self.sm_obj_label = FCLabel("%s:" % _("Select Soldermask 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' grid_lay_1.addWidget(self.sm_obj_label, 14, 0, 1, 3) grid_lay_1.addWidget(self.sm_object_combo, 16, 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_1.addWidget(self.clearance_ppm_label, 18, 0) grid_lay_1.addWidget(self.clearance_ppm_entry, 18, 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.setDisabled(True) if self.units.upper() == 'MM': self.units_area_label = FCLabel('%s2' % _("mm")) else: self.units_area_label = FCLabel('%s2' % _("in")) grid_lay_1.addWidget(self.plated_area_label, 20, 0) grid_lay_1.addWidget(self.plated_area_entry, 20, 1) grid_lay_1.addWidget(self.units_area_label, 20, 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_radio = RadioSet([ {"label": _("Both"), "value": "b"}, {'label': _('Thieving'), 'value': 't'}, {"label": _("Robber bar"), "value": "r"}, {"label": _("None"), "value": "n"} ], orientation='vertical', stretch=False) grid_lay_1.addWidget(self.ppm_choice_label, 22, 0) grid_lay_1.addWidget(self.ppm_choice_radio, 22, 1, 1, 2) # ## Pattern Plating Mask 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; } """) grid_lay_1.addWidget(self.ppm_button, 24, 0, 1, 3) self.layout.addStretch() # ## 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)