# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 11/21/2019 # # MIT Licence # # ########################################################## from PyQt6 import QtWidgets, QtCore, QtGui from appTool import AppTool from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, FCCheckBox, \ FCComboBox2, RadioSet, FCDoubleSpinner, EvalEntry, FCTable from appCommon.Common import LoudDict import logging from copy import deepcopy import math from shapely import LineString, Polygon, MultiPolygon, box, Point from shapely.geometry import base from shapely.ops import unary_union import gettext import appTranslation as fcTranslate import builtins from camlib import flatten_shapely_geometry fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class ToolFiducials(AppTool): def __init__(self, app): AppTool.__init__(self, app) self.app = app self.canvas = self.app.plotcanvas self.cursor_color_memory = None # store the current cursor type to be restored after manual geo self.old_cursor_type = self.app.options["global_cursor_type"] self.decimals = self.app.decimals self.units = '' # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = FidoUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName # Objects involved in Copper thieving self.grb_object = None self.sm_object = None self.copper_obj_set = set() self.sm_obj_set = set() # store the flattened geometry here: self.flat_geometry = [] # Events ID self.mr = None self.mm = None # Mouse cursor positions self.cursor_pos = (0, 0) self.first_click = False self.mode_method = False # Tool properties self.fid_dia = None self.sm_opening_dia = None self.margin_val = None self.sec_position = None self.grb_steps_per_circle = self.app.options["gerber_circle_steps"] self.click_points = [] self.handlers_connected = False # storage for temporary shapes when adding manual markers self.temp_shapes = self.app.sel_shapes def run(self, toggle=True): self.app.defaults.report_usage("ToolFiducials()") if toggle: # if the splitter is hidden, display it if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same found_idx = None for idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": found_idx = idx break # show the Tab if not found_idx: try: self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) except RuntimeError: self.app.ui.plugin_tab = QtWidgets.QWidget() self.app.ui.plugin_tab.setObjectName("plugin_tab") self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) self.app.ui.plugin_scroll_area = VerticalScrollArea() self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) try: if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx: # if the Tool Tab is not focused, focus on it if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab: # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) else: # else remove the Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) self.app.ui.notebook.removeTab(2) # if there are no objects loaded in the app then hide the Notebook widget if not self.app.collection.get_list(): self.app.ui.splitter.setSizes([0, 1]) except AttributeError: pass else: if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) super().run() self.set_tool_ui() self.app.ui.notebook.setTabText(2, _("Fiducials")) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+F', **kwargs) def connect_signals_at_init(self): # ############################################################################# # ############################ SIGNALS ######################################## # ############################################################################# self.ui.level.toggled.connect(self.on_level_changed) self.ui.add_cfid_button.clicked.connect(self.add_fiducials) self.ui.add_sm_opening_button.clicked.connect(self.add_soldermask_opening) self.ui.fid_type_combo.currentIndexChanged.connect(self.on_fiducial_type) self.ui.pos_radio.activated_custom.connect(self.on_second_point) self.ui.mode_radio.activated_custom.connect(self.on_method_change) self.ui.big_cursor_cb.stateChanged.connect(self.on_cursor_change) self.ui.reset_button.clicked.connect(self.set_tool_ui) def set_tool_ui(self): self.units = self.app.app_units self.clear_ui(self.layout) self.ui = FidoUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.ui.fid_size_entry.set_value(self.app.options["tools_fiducials_dia"]) self.ui.margin_entry.set_value(float(self.app.options["tools_fiducials_margin"])) self.ui.mode_radio.set_value(self.app.options["tools_fiducials_mode"]) self.ui.pos_radio.set_value(self.app.options["tools_fiducials_second_pos"]) self.ui.fid_type_combo.set_value(self.app.options["tools_fiducials_type"]) # needed so the visibility of some objects will be updated self.on_fiducial_type(val=self.ui.fid_type_combo.get_value()) self.ui.line_thickness_entry.set_value(float(self.app.options["tools_fiducials_line_thickness"])) self.click_points = [] self.ui.bottom_left_coords_entry.set_value('') self.ui.top_right_coords_entry.set_value('') self.ui.sec_points_coords_entry.set_value('') self.copper_obj_set = set() self.sm_obj_set = set() # Show/Hide Advanced Options app_mode = self.app.options["global_app_level"] self.change_level(app_mode) # 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) self.ui.big_cursor_cb.set_value(self.app.options["tools_fiducials_big_cursor"]) # set cursor self.old_cursor_type = self.app.options["global_cursor_type"] def change_level(self, level): """ :param level: application level: either 'b' or 'a' :type level: str :return: """ if level == 'a': self.ui.level.setChecked(True) else: self.ui.level.setChecked(False) self.on_level_changed(self.ui.level.isChecked()) def on_level_changed(self, checked): if not checked: self.ui.level.setText('%s' % _('Beginner')) self.ui.level.setStyleSheet(""" QToolButton { color: green; } """) self.ui.separator_line.hide() self.ui.fid_type_label.hide() self.ui.fid_type_combo.hide() self.ui.line_thickness_label.hide() self.ui.line_thickness_entry.hide() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.level.setStyleSheet(""" QToolButton { color: red; } """) self.ui.separator_line.show() self.ui.fid_type_label.show() self.ui.fid_type_combo.show() self.ui.line_thickness_label.show() self.ui.line_thickness_entry.show() def on_second_point(self, val): if val == 'no': self.ui.id_item_3.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) self.ui.sec_points_coords_entry.setDisabled(True) else: self.ui.id_item_3.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.sec_points_coords_entry.setDisabled(False) def on_method_change(self, val): """ Make sure that on method change we disconnect the event handlers and reset the points storage :param val: value of the Radio button which trigger this method :return: None """ self.click_points = [] if val == 'auto': try: self.disconnect_event_handlers() except TypeError: pass self.ui.big_cursor_cb.hide() else: self.ui.big_cursor_cb.show() def on_cursor_change(self, val): if val: self.app.options['tools_fiducials_big_cursor'] = True else: self.app.options['tools_fiducials_big_cursor'] = False def on_fiducial_type(self, val): if val == 2: # 'cross' self.ui.line_thickness_label.setDisabled(False) self.ui.line_thickness_entry.setDisabled(False) else: self.ui.line_thickness_label.setDisabled(True) self.ui.line_thickness_entry.setDisabled(True) def add_fiducials(self): self.app.call_source = "fiducials_tool" self.app.ui.notebook.setDisabled(True) self.mode_method = self.ui.mode_radio.get_value() self.margin_val = self.ui.margin_entry.get_value() self.sec_position = self.ui.pos_radio.get_value() fid_type = self.ui.fid_type_combo.get_value() self.click_points = [] # get the Gerber object on which the Fiducial 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("ToolFiducials.execute() --> %s" % str(e)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) self.app.ui.notebook.setDisabled(False) self.app.call_source = "app" return self.copper_obj_set.add(self.grb_object.obj_options['name']) if self.mode_method == 'auto': xmin, ymin, xmax, ymax = self.grb_object.bounds() bbox = box(xmin, ymin, xmax, ymax) buf_bbox = bbox.buffer(self.margin_val, self.grb_steps_per_circle, join_style="mitre") x0, y0, x1, y1 = buf_bbox.bounds self.click_points.append( ( float('%.*f' % (self.decimals, x0)), float('%.*f' % (self.decimals, y0)) ) ) self.ui.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0)) self.click_points.append( ( float('%.*f' % (self.decimals, x1)), float('%.*f' % (self.decimals, y1)) ) ) self.ui.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1)) if self.sec_position == 'up': self.click_points.append( ( float('%.*f' % (self.decimals, x0)), float('%.*f' % (self.decimals, y1)) ) ) self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1)) elif self.sec_position == 'down': self.click_points.append( ( float('%.*f' % (self.decimals, x1)), float('%.*f' % (self.decimals, y0)) ) ) self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0)) ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type) self.app.call_source = "app" if ret_val == 'fail': self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) self.disconnect_event_handlers() self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return self.on_exit() else: self.app.inform.emit(_("Click to add first Fiducial. Bottom Left...")) self.ui.bottom_left_coords_entry.set_value('') self.ui.top_right_coords_entry.set_value('') self.ui.sec_points_coords_entry.set_value('') if self.ui.big_cursor_cb.get_value(): self.app.on_cursor_type(val="big", control_cursor=True) self.cursor_color_memory = self.app.plotcanvas.cursor_color if self.app.use_3d_engine is True: self.app.plotcanvas.cursor_color = '#000000FF' else: self.app.plotcanvas.cursor_color = '#000000' self.app.app_cursor.enabled = True else: self.app.on_cursor_type(val="small", control_cursor=True) self.app.plotcanvas.cursor_color = self.cursor_color_memory self.connect_event_handlers() # To be called after clicking on the plot. def add_fiducials_geo(self, points_list, g_obj, fid_size=None, fid_type=None, line_size=None): """ Add geometry to the solid_geometry of the copper Gerber object :param points_list: list of coordinates for the fiducials :param g_obj: the Gerber object where to add the geometry :param fid_size: the overall size of the fiducial or fiducial opening depending on the g_obj type :param fid_type: the type of fiducial: circular, cross, chess :param line_size: the line thickenss when the fiducial type is cross :return: """ fid_size = self.ui.fid_size_entry.get_value() if fid_size is None else fid_size fid_type = 0 if fid_type is None else fid_type # default is 'circular' <=> 0 if fid_type == "circular": fid_type = 0 line_thickness = self.ui.line_thickness_entry.get_value() if line_size is None else line_size radius = fid_size / 2.0 new_apertures = deepcopy(g_obj.tools) if fid_type == 0: # 'circular' geo_list = [Point(pt).buffer(radius, self.grb_steps_per_circle) for pt in points_list] aperture_found = None for ap_id, ap_val in g_obj.tools.items(): if ap_val['type'] == 'C' and ap_val['size'] == fid_size: aperture_found = ap_id break if aperture_found: for geo in geo_list: dict_el = {'follow': geo.centroid, 'solid': geo} new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el)) else: ap_keys = list(g_obj.tools.keys()) if ap_keys: new_apid = int(max(ap_keys)) + 1 else: new_apid = 10 new_apertures[new_apid] = { 'type': 'C', 'size': fid_size, 'geometry': [] } for geo in geo_list: dict_el = {'follow': geo.centroid, 'solid': geo} new_apertures[new_apid]['geometry'].append(deepcopy(dict_el)) s_list = [] flat_geo = flatten_shapely_geometry(g_obj.solid_geometry) if flat_geo: for poly in flat_geo: s_list.append(poly) s_list += geo_list elif fid_type == 1: # 'cross' geo_list = [] for pt in points_list: x = pt[0] y = pt[1] line_geo_hor = LineString([ (x - radius + (line_thickness / 2.0), y), (x + radius - (line_thickness / 2.0), y) ]) line_geo_vert = LineString([ (x, y - radius + (line_thickness / 2.0)), (x, y + radius - (line_thickness / 2.0)) ]) geo_list.append([line_geo_hor, line_geo_vert]) aperture_found = None for ap_id, ap_val in g_obj.tools.items(): if ap_val['type'] == 'C' and ap_val['size'] == line_thickness: aperture_found = ap_id break geo_buff_list = [] if aperture_found: for geo in geo_list: geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle) geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle) geo_buff_list.append(geo_buff_h) geo_buff_list.append(geo_buff_v) dict_el = {'follow': geo_buff_h.centroid, 'solid': geo_buff_h} new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el)) dict_el = {'follow': geo_buff_v.centroid, 'solid': geo_buff_v} new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el)) else: ap_keys = list(g_obj.tools.keys()) if ap_keys: new_apid = int(max(ap_keys)) + 1 else: new_apid = 10 new_apertures[new_apid] = { 'type': 'C', 'size': line_thickness, 'geometry': [] } for geo in geo_list: geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle) geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle) geo_buff_list.append(geo_buff_h) geo_buff_list.append(geo_buff_v) dict_el = {'follow': geo_buff_h.centroid, 'solid': geo_buff_h} new_apertures[new_apid]['geometry'].append(deepcopy(dict_el)) dict_el = {'follow': geo_buff_v.centroid, 'solid': geo_buff_v} new_apertures[new_apid]['geometry'].append(deepcopy(dict_el)) s_list = [] flat_geo = flatten_shapely_geometry(g_obj.solid_geometry) if flat_geo: for poly in flat_geo: s_list.append(poly) geo_buff_list = MultiPolygon(geo_buff_list) geo_buff_list = geo_buff_list.buffer(0) geo_buff_list = flatten_shapely_geometry(geo_buff_list) for poly in geo_buff_list: s_list.append(poly) else: # value 3 meaning 'chess' pattern fiducial type geo_list = [] def make_square_poly(center_pt, side_size): """ :param center_pt: :param side_size: :return: Polygon """ half_s = side_size / 2 x_center = center_pt[0] y_center = center_pt[1] pt1 = (x_center - half_s, y_center - half_s) pt2 = (x_center + half_s, y_center - half_s) pt3 = (x_center + half_s, y_center + half_s) pt4 = (x_center - half_s, y_center + half_s) return Polygon([pt1, pt2, pt3, pt4, pt1]) for pt in points_list: x = pt[0] y = pt[1] first_square = make_square_poly(center_pt=(x-fid_size/4, y+fid_size/4), side_size=fid_size/2) second_square = make_square_poly(center_pt=(x+fid_size/4, y-fid_size/4), side_size=fid_size/2) geo_list += [first_square, second_square] aperture_found = None new_ap_size = math.sqrt(fid_size**2 + fid_size**2) for ap_id, ap_val in g_obj.tools.items(): if ap_val['type'] == 'R' and \ round(ap_val['size'], ndigits=self.decimals) == round(new_ap_size, ndigits=self.decimals): aperture_found = ap_id break geo_buff_list = [] if aperture_found: for geo in geo_list: assert isinstance(geo, base.BaseGeometry) geo_buff_list.append(geo) dict_el = {'follow': geo.centroid, 'solid': geo} new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el)) else: ap_keys = list(g_obj.tools.keys()) if ap_keys: new_apid = int(max(ap_keys)) + 1 else: new_apid = 10 new_apertures[new_apid] = { 'type': 'R', 'size': new_ap_size, 'width': fid_size, 'height': fid_size, 'geometry': [] } for geo in geo_list: assert isinstance(geo, base.BaseGeometry) geo_buff_list.append(geo) dict_el = {'follow': geo.centroid, 'solid': geo} new_apertures[new_apid]['geometry'].append(deepcopy(dict_el)) s_list = [] flat_geo = flatten_shapely_geometry(g_obj.solid_geometry) if flat_geo: for poly in flat_geo: s_list.append(poly) for poly in geo_buff_list: s_list.append(poly) outname = '%s_%s' % (str(g_obj.obj_options['name']), 'fid') def initialize(grb_obj, app_obj): grb_obj.obj_options = LoudDict() for opt in g_obj.obj_options: if opt != 'name': grb_obj.obj_options[opt] = deepcopy(g_obj.obj_options[opt]) grb_obj.obj_options['name'] = outname grb_obj.multitool = False grb_obj.multigeo = False grb_obj.follow = deepcopy(g_obj.follow) grb_obj.tools = new_apertures grb_obj.solid_geometry = unary_union(s_list) grb_obj.follow_geometry = deepcopy(g_obj.follow_geometry) + geo_list grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj, use_thread=False) ret = self.app.app_obj.new_object('gerber', outname, initialize, plot=True) return ret def add_soldermask_opening(self): sm_opening_dia = self.ui.fid_size_entry.get_value() * 2.0 # get the Gerber object on which the Fiducial 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.sm_object = model_index.internalPointer().obj except Exception as e: self.app.log.error("ToolFiducials.add_soldermask_opening() --> %s" % str(e)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return self.sm_obj_set.add(self.sm_object.obj_options['name']) ret_val = self.add_fiducials_geo( self.click_points, g_obj=self.sm_object, fid_size=sm_opening_dia, fid_type='circular') self.app.call_source = "app" if ret_val == 'fail': self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) self.disconnect_event_handlers() self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return self.on_exit() def on_mouse_release(self, event): if self.app.use_3d_engine: event_pos = event.pos right_button = 2 self.app.event_is_dragging = self.app.event_is_dragging else: event_pos = (event.xdata, event.ydata) right_button = 3 self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning if event.button == 1: pos_canvas = self.canvas.translate_coords(event_pos) if self.app.grid_status(): pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) else: pos = (pos_canvas[0], pos_canvas[1]) click_pt = Point([pos[0], pos[1]]) self.click_points.append( ( float('%.*f' % (self.decimals, click_pt.x)), float('%.*f' % (self.decimals, click_pt.y)) ) ) self.check_points() self.draw_utility_geometry(pos=pos) elif event.button == right_button and self.app.event_is_dragging is False: self.on_exit(cancelled=True) def check_points(self): fid_type = self.ui.fid_type_combo.get_value() if len(self.click_points) == 1: self.ui.bottom_left_coords_entry.set_value(self.click_points[0]) self.app.inform.emit(_("Click to add the last fiducial. Top Right...")) if self.sec_position != 'no': if len(self.click_points) == 2: self.ui.top_right_coords_entry.set_value(self.click_points[1]) self.app.inform.emit(_("Click to add the second fiducial. Top Left or Bottom Right...")) elif len(self.click_points) == 3: self.ui.sec_points_coords_entry.set_value(self.click_points[2]) self.app.inform.emit('[success] %s' % _("Done.")) ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type) self.app.call_source = "app" if ret_val == 'fail': self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) self.disconnect_event_handlers() self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return self.on_exit() else: if len(self.click_points) == 2: self.ui.top_right_coords_entry.set_value(self.click_points[1]) self.app.inform.emit('[success] %s' % _("Done.")) ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type) self.app.call_source = "app" if ret_val == 'fail': self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) self.disconnect_event_handlers() self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return self.on_exit() def on_mouse_move(self, event): pass 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 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_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.mr) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) 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) else: self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) self.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 def on_exit(self, cancelled=None): # restore cursor self.app.on_cursor_type(val=self.old_cursor_type, control_cursor=False) self.app.plotcanvas.cursor_color = self.cursor_color_memory self.clear_utility_geometry() # plot the object for ob_name in self.copper_obj_set: try: copper_obj = self.app.collection.get_by_name(name=ob_name) if len(self.copper_obj_set) > 1: self.replot(obj=copper_obj, run_thread=False) else: self.replot(obj=copper_obj) except (AttributeError, TypeError): continue # update the bounding box values try: a, b, c, d = copper_obj.bounds() copper_obj.obj_options['xmin'] = a copper_obj.obj_options['ymin'] = b copper_obj.obj_options['xmax'] = c copper_obj.obj_options['ymax'] = d except Exception as e: self.app.log.error("ToolFiducials.on_exit() copper_obj bounds error --> %s" % str(e)) for ob_name in self.sm_obj_set: try: sm_obj = self.app.collection.get_by_name(name=ob_name) if len(self.sm_obj_set) > 1: self.replot(obj=sm_obj, run_thread=False) else: self.replot(obj=sm_obj) except (AttributeError, TypeError): continue # update the bounding box values try: a, b, c, d = sm_obj.bounds() sm_obj.obj_options['xmin'] = a sm_obj.obj_options['ymin'] = b sm_obj.obj_options['xmax'] = c sm_obj.obj_options['ymax'] = d except Exception as e: self.app.log.error("ToolFiducials.on_exit() sm_obj bounds error --> %s" % str(e)) # Events ID self.mr = None # self.mm = None # Mouse cursor positions self.cursor_pos = (0, 0) self.first_click = False self.disconnect_event_handlers() self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) if cancelled is True: self.app.delete_selection_shape() self.disconnect_event_handlers() self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request.")) return self.app.inform.emit('[success] %s' % _("Fiducials Tool exit.")) def draw_utility_geometry(self, pos): fid_type = self.ui.fid_type_combo.get_value() line_thickness = self.ui.line_thickness_entry.get_value() fid_size = self.ui.fid_size_entry.get_value() radius = fid_size / 2.0 geo_list = [] pt = pos[0], pos[1] # we ensure that this works in case the pt tuple is a return from Vispy (4 members) if fid_type == 0: # 'circular' geo_list = [Point(pt).buffer(radius, self.grb_steps_per_circle)] elif fid_type == 1: # 'cross' x = pt[0] y = pt[1] line_geo_hor = LineString([ (x - radius + (line_thickness / 2.0), y), (x + radius - (line_thickness / 2.0), y) ]) line_geo_vert = LineString([ (x, y - radius + (line_thickness / 2.0)), (x, y + radius - (line_thickness / 2.0)) ]) geo_list = [line_geo_hor, line_geo_vert] else: # 'chess' pattern def make_square_poly(center_pt, side_size): """ :param center_pt: :param side_size: :return: Polygon """ half_s = side_size / 2 x_center = center_pt[0] y_center = center_pt[1] pt1 = (x_center - half_s, y_center - half_s) pt2 = (x_center + half_s, y_center - half_s) pt3 = (x_center + half_s, y_center + half_s) pt4 = (x_center - half_s, y_center + half_s) return Polygon([pt1, pt2, pt3, pt4, pt1]) x = pt[0] y = pt[1] first_square = make_square_poly(center_pt=(x - fid_size / 4, y + fid_size / 4), side_size=fid_size / 2) second_square = make_square_poly(center_pt=(x + fid_size / 4, y - fid_size / 4), side_size=fid_size / 2) geo_list += [first_square, second_square] outline = '#0000FFAF' for util_geo in geo_list: self.temp_shapes.add(util_geo, color=outline, update=True, layer=0, tolerance=None) if self.app.use_3d_engine: self.temp_shapes.redraw() def clear_utility_geometry(self): self.temp_shapes.clear(update=True) self.temp_shapes.redraw() def on_plugin_cleanup(self): self.on_exit() class FidoUI: pluginName = _("Fiducials") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.layout = layout self.title_box = QtWidgets.QHBoxLayout() self.layout.addLayout(self.title_box) # ## Title title_label = FCLabel("%s" % self.pluginName, size=16, bold=True) self.title_box.addWidget(title_label) # App Level label self.level = QtWidgets.QToolButton() self.level.setToolTip( _( "Beginner Mode - many parameters are hidden.\n" "Advanced Mode - full control.\n" "Permanent change is done in 'Preferences' menu." ) ) self.level.setCheckable(True) self.title_box.addWidget(self.level) self.tools_frame = QtWidgets.QFrame() self.tools_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tools_frame) self.tools_box = QtWidgets.QVBoxLayout() self.tools_box.setContentsMargins(0, 0, 0, 0) self.tools_frame.setLayout(self.tools_box) self.title_box = QtWidgets.QHBoxLayout() self.tools_box.addLayout(self.title_box) # ############################################################################################################# # Gerber Source Object # ############################################################################################################# self.obj_combo_label = FCLabel('%s' % _("Source Object"), color='darkorange', bold=True) self.obj_combo_label.setToolTip( _("Gerber object for adding fiducials and soldermask openings.") ) 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.tools_box.addWidget(self.obj_combo_label) self.tools_box.addWidget(self.grb_object_combo) # ############################################################################################################# # Coordinates Table Frame # ############################################################################################################# self.points_label = FCLabel('%s' % _("Coordinates"), color='green', bold=True) self.points_label.setToolTip( _("A table with the fiducial points coordinates,\n" "in the format (x, y).") ) self.tools_box.addWidget(self.points_label) self.points_table = FCTable() self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) self.tools_box.addWidget(self.points_table) self.points_table.setColumnCount(3) self.points_table.setHorizontalHeaderLabels( [ '#', _("Name"), _("Coordinates"), ] ) self.points_table.setRowCount(3) row = 0 flags = QtCore.Qt.ItemFlag.ItemIsEnabled # BOTTOM LEFT id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1) id_item_1.setFlags(flags) self.points_table.setItem(row, 0, id_item_1) # Tool name/id self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left')) self.bottom_left_coords_lbl.setFlags(flags) self.points_table.setItem(row, 1, self.bottom_left_coords_lbl) self.bottom_left_coords_entry = EvalEntry() self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry) row += 1 # TOP RIGHT id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2) id_item_2.setFlags(flags) self.points_table.setItem(row, 0, id_item_2) # Tool name/id self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right')) self.top_right_coords_lbl.setFlags(flags) self.points_table.setItem(row, 1, self.top_right_coords_lbl) self.top_right_coords_entry = EvalEntry() self.points_table.setCellWidget(row, 2, self.top_right_coords_entry) row += 1 # Second Point self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3) self.id_item_3.setFlags(flags) self.points_table.setItem(row, 0, self.id_item_3) # Tool name/id self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point')) self.sec_point_coords_lbl.setFlags(flags) self.points_table.setItem(row, 1, self.sec_point_coords_lbl) self.sec_points_coords_entry = EvalEntry() self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry) vertical_header = self.points_table.verticalHeader() vertical_header.hide() self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) horizontal_header = self.points_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) # for x in range(4): # self.points_table.resizeColumnToContents(x) self.points_table.resizeColumnsToContents() self.points_table.resizeRowsToContents() horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Fixed) horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) self.points_table.setMinimumHeight(self.points_table.getHeight() + 2) self.points_table.setMaximumHeight(self.points_table.getHeight() + 2) # remove the frame on the QLineEdit children of the table for row in range(self.points_table.rowCount()): wdg = self.points_table.cellWidget(row, 2) assert isinstance(wdg, QtWidgets.QLineEdit) wdg.setFrame(False) # separator_line = QtWidgets.QFrame() # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # self.layout.addWidget(separator_line) # ############################################################################################################# # Parameters Frame # ############################################################################################################# self.param_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True) self.param_label.setToolTip( _("Parameters used for this tool.") ) self.tools_box.addWidget(self.param_label) par_frame = FCFrame() self.tools_box.addWidget(par_frame) param_grid = GLay(v_spacing=5, h_spacing=3) par_frame.setLayout(param_grid) # DIAMETER # self.size_label = FCLabel('%s:' % _("Size")) self.size_label.setToolTip( _("This set the fiducial diameter if fiducial type is circular,\n" "otherwise is the size of the fiducial.\n" "The soldermask opening is double than that.") ) self.fid_size_entry = FCDoubleSpinner(callback=self.confirmation_message) self.fid_size_entry.set_range(1.0000, 3.0000) self.fid_size_entry.set_precision(self.decimals) self.fid_size_entry.setWrapping(True) self.fid_size_entry.setSingleStep(0.1) param_grid.addWidget(self.size_label, 2, 0) param_grid.addWidget(self.fid_size_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(-10000.0000, 10000.0000) self.margin_entry.set_precision(self.decimals) self.margin_entry.setSingleStep(0.1) param_grid.addWidget(self.margin_label, 4, 0) param_grid.addWidget(self.margin_entry, 4, 1) # Position for second fiducial # self.pos_radio = RadioSet([ {'label': _('Up'), 'value': 'up'}, {"label": _("Down"), "value": "down"}, {"label": _("None"), "value": "no"} ], compact=True) self.pos_label = FCLabel('%s:' % _("Second fiducial")) self.pos_label.setToolTip( _("The position for the second fiducial.\n" "- 'Up' - the order is: bottom-left, top-left, top-right.\n" "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n" "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.") ) param_grid.addWidget(self.pos_label, 6, 0) param_grid.addWidget(self.pos_radio, 6, 1) self.separator_line = QtWidgets.QFrame() self.separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) param_grid.addWidget(self.separator_line, 8, 0, 1, 2) # Fiducial type # self.fid_type_label = FCLabel('%s:' % _("Fiducial Type")) self.fid_type_label.setToolTip( _("The type of fiducial.\n" "- 'Circular' - this is the regular fiducial.\n" "- 'Cross' - cross lines fiducial.\n" "- 'Chess' - chess pattern fiducial.") ) self.fid_type_combo = FCComboBox2() self.fid_type_combo.addItems([_('Circular'), _("Cross"), _("Chess")]) param_grid.addWidget(self.fid_type_label, 10, 0) param_grid.addWidget(self.fid_type_combo, 10, 1) # Line Thickness # self.line_thickness_label = FCLabel('%s:' % _("Line thickness")) self.line_thickness_label.setToolTip( _("Thickness of the line that makes the fiducial.") ) self.line_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message) self.line_thickness_entry.set_range(0.00001, 10000.0000) self.line_thickness_entry.set_precision(self.decimals) self.line_thickness_entry.setSingleStep(0.1) param_grid.addWidget(self.line_thickness_label, 12, 0) param_grid.addWidget(self.line_thickness_entry, 12, 1) # separator_line_1 = QtWidgets.QFrame() # separator_line_1.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line_1.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # param_grid.addWidget(separator_line_1, 14, 0, 1, 2) # ############################################################################################################# # Selection Frame # ############################################################################################################# self.sel_label = FCLabel('%s' % _("Selection"), color='green', bold=True) self.tools_box.addWidget(self.sel_label) self.s_frame = FCFrame() self.tools_box.addWidget(self.s_frame) # Grid Layout grid_sel = GLay(v_spacing=5, h_spacing=3) self.s_frame.setLayout(grid_sel) # Mode # self.mode_radio = RadioSet([ {'label': _('Auto'), 'value': 'auto'}, {"label": _("Manual"), "value": "manual"} ], compact=True) self.mode_label = FCLabel(_("Mode:")) self.mode_label.setToolTip( _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n" "- 'Manual' - manual placement of fiducials.") ) grid_sel.addWidget(self.mode_label, 0, 0) grid_sel.addWidget(self.mode_radio, 0, 1) # Big Cursor self.big_cursor_cb = FCCheckBox('%s' % _("Big cursor")) self.big_cursor_cb.setToolTip( _("Use a big cursor.")) grid_sel.addWidget(self.big_cursor_cb, 2, 0, 1, 2) GLay.set_common_column_size([grid_sel, param_grid, param_grid], 0) # ## Insert Copper Fiducial self.add_cfid_button = FCButton(_("Add Fiducial"), bold=True) self.add_cfid_button.setIcon(QtGui.QIcon(self.app.resource_location + '/fiducials_32.png')) self.add_cfid_button.setToolTip( _("Will add a polygon on the copper layer to serve as fiducial.") ) self.tools_box.addWidget(self.add_cfid_button) # ## Insert Soldermask opening for Fiducial self.add_sm_opening_button = FCButton(_("Add Soldermask Opening"), bold=True) self.add_sm_opening_button.setToolTip( _("Will add a polygon on the soldermask layer\n" "to serve as fiducial opening.\n" "The diameter is always double of the diameter\n" "for the copper fiducial.") ) self.tools_box.addWidget(self.add_sm_opening_button) self.layout.addStretch(1) # ## Reset Tool self.reset_button = FCButton(_("Reset Tool"), bold=True) self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png')) self.reset_button.setToolTip( _("Will reset the tool parameters.") ) self.layout.addWidget(self.reset_button) # #################################### 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)