from PyQt6 import QtWidgets, QtCore, QtGui from appTool import AppTool from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCButton, FCComboBox, NumericalEvalTupleEntry, FCLabel, \ VerticalScrollArea, FCGridLayout, FCFrame from numpy import Inf from copy import deepcopy from shapely.geometry import Point from shapely import affinity import logging import gettext import appTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class DblSidedTool(AppTool): def __init__(self, app): AppTool.__init__(self, app) self.decimals = self.app.decimals self.canvas = self.app.plotcanvas # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = DsidedUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.mr = None self.drill_values = "" # will hold the Excellon object used for picking a hole as mirror reference self.exc_hole_obj = None # store the status of the grid self.grid_status_memory = None # set True if mouse events are locally connected self.local_connected = False def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs) def run(self, toggle=True): self.app.defaults.report_usage("Tool2Sided()") if toggle: # if the splitter is hidden, display it if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same found_idx = None for idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": found_idx = idx break # show the Tab if not found_idx: try: self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) except RuntimeError: self.app.ui.plugin_tab = QtWidgets.QWidget() self.app.ui.plugin_tab.setObjectName("plugin_tab") self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) self.app.ui.plugin_scroll_area = VerticalScrollArea() self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) try: if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx: # if the Tool Tab is not focused, focus on it if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab: # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) else: # else remove the Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) self.app.ui.notebook.removeTab(2) # if there are no objects loaded in the app then hide the Notebook widget if not self.app.collection.get_list(): self.app.ui.splitter.setSizes([0, 1]) except AttributeError: pass else: if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) AppTool.run(self) self.set_tool_ui() self.app.ui.notebook.setTabText(2, _("2-Sided")) def connect_signals_at_init(self): # ############################################################################# # ############################ SIGNALS ######################################## # ############################################################################# self.ui.level.toggled.connect(self.on_level_changed) self.ui.object_type_radio.activated_custom.connect(self.on_object_type) self.ui.add_point_button.clicked.connect(self.on_point_add) self.ui.add_drill_point_button.clicked.connect(self.on_drill_add) self.ui.delete_drill_point_button.clicked.connect(self.on_drill_delete_last) self.ui.box_type_radio.activated_custom.connect(self.on_combo_box_type) self.ui.axis_location.group_toggle_fn = self.on_toggle_pointbox self.ui.point_entry.textChanged.connect(lambda val: self.ui.align_ref_label_val.set_value(val)) self.ui.pick_hole_button.clicked.connect(self.on_pick_hole) self.ui.mirror_button.clicked.connect(self.on_mirror) self.ui.xmin_btn.clicked.connect(self.on_xmin_clicked) self.ui.ymin_btn.clicked.connect(self.on_ymin_clicked) self.ui.xmax_btn.clicked.connect(self.on_xmax_clicked) self.ui.ymax_btn.clicked.connect(self.on_ymax_clicked) self.ui.center_btn.clicked.connect( lambda: self.ui.point_entry.set_value(self.ui.center_entry.get_value()) ) self.ui.create_excellon_button.clicked.connect(self.on_create_alignment_holes) self.ui.calculate_bb_button.clicked.connect(self.on_bbox_coordinates) self.app.proj_selection_changed.connect(self.on_object_selection_changed) self.ui.reset_button.clicked.connect(self.set_tool_ui) def set_tool_ui(self): self.clear_ui(self.layout) self.ui = DsidedUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() self.reset_fields() self.ui.point_entry.set_value("") self.ui.alignment_holes.set_value("") self.ui.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"]) self.ui.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"]) self.ui.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"]) self.ui.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"]) self.ui.xmin_entry.set_value(0.0) self.ui.ymin_entry.set_value(0.0) self.ui.xmax_entry.set_value(0.0) self.ui.ymax_entry.set_value(0.0) self.ui.center_entry.set_value('') self.ui.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0)) # SELECT THE CURRENT OBJECT obj = self.app.collection.get_active() if obj: obj_name = obj.options['name'] if obj.kind == 'gerber': # run once to make sure that the obj_type attribute is updated in the FCComboBox self.ui.object_type_radio.set_value('grb') self.on_object_type('grb') self.ui.box_type_radio.set_value('grb') self.on_combo_box_type('grb') elif obj.kind == 'excellon': # run once to make sure that the obj_type attribute is updated in the FCComboBox self.ui.object_type_radio.set_value('exc') self.on_object_type('exc') self.ui.box_type_radio.set_value('exc') self.on_combo_box_type('exc') elif obj.kind == 'geometry': # run once to make sure that the obj_type attribute is updated in the FCComboBox self.ui.object_type_radio.set_value('geo') self.on_object_type('geo') self.ui.box_type_radio.set_value('geo') self.on_combo_box_type('geo') self.ui.object_combo.set_value(obj_name) else: self.ui.object_type_radio.set_value('grb') self.on_object_type('grb') if self.local_connected is True: self.disconnect_events() # Show/Hide Advanced Options app_mode = self.app.defaults["global_app_level"] self.change_level(app_mode) def change_level(self, level): """ :param level: application level: either 'b' or 'a' :type level: str :return: """ if level == 'a': self.ui.level.setChecked(True) else: self.ui.level.setChecked(False) self.on_level_changed(self.ui.level.isChecked()) def on_level_changed(self, checked): if not checked: self.ui.level.setText('%s' % _('Beginner')) self.ui.level.setStyleSheet(""" QToolButton { color: green; } """) self.ui.bv_label.hide() self.ui.bounds_frame.hide() self.ui.center_entry.hide() self.ui.center_btn.hide() self.ui.calculate_bb_button.hide() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.level.setStyleSheet(""" QToolButton { color: red; } """) self.ui.bv_label.show() self.ui.bounds_frame.show() self.ui.center_entry.show() self.ui.center_btn.show() self.ui.calculate_bb_button.show() def on_object_type(self, val): obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val] self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.object_combo.setCurrentIndex(0) self.ui.object_combo.obj_type = { "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val] def on_combo_box_type(self, val): obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val] self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.box_combo.setCurrentIndex(0) self.ui.box_combo.obj_type = { "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val] def on_object_selection_changed(self, current, previous): try: name = current.indexes()[0].internalPointer().obj.options['name'] kind = current.indexes()[0].internalPointer().obj.kind if kind in ['gerber', 'excellon', 'geometry']: obj_type = {'gerber': 'grb', 'excellon': 'exc', 'geometry': 'geo'}[kind] self.ui.object_type_radio.set_value(obj_type) self.ui.box_type_radio.set_value(obj_type) self.ui.object_combo.set_value(name) except Exception: pass def on_create_alignment_holes(self): axis = self.ui.align_axis_radio.get_value() mode = self.ui.axis_location.get_value() if mode == "point": try: px, py = self.ui.point_entry.get_value() except TypeError: msg = '[WARNING_NOTCL] %s' % \ _("'Point' reference is selected and 'Point' coordinates are missing. Add them and retry.") self.app.inform.emit(msg) return else: selection_index = self.ui.box_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.ui.object_combo.rootModelIndex()) try: bb_obj = model_index.internalPointer().obj except AttributeError: msg = '[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry.") self.app.inform.emit(msg) return xmin, ymin, xmax, ymax = bb_obj.bounds() px = 0.5 * (xmin + xmax) py = 0.5 * (ymin + ymax) xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] dia = self.ui.drill_dia.get_value() if dia == '': msg = '[WARNING_NOTCL] %s' % _("No value or wrong format in Drill Dia entry. Add it and retry.") self.app.inform.emit(msg) return tools = {1: {}} tools[1]["tooldia"] = dia tools[1]['drills'] = [] tools[1]['solid_geometry'] = [] # holes = self.alignment_holes.get_value() holes = eval('[{}]'.format(self.ui.alignment_holes.text())) if not holes: msg = '[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. Add them and retry.") self.app.inform.emit(msg) return for hole in holes: point = Point(hole) point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py)) tools[1]['drills'] += [point, point_mirror] tools[1]['solid_geometry'] += [point, point_mirror] def obj_init(obj_inst, app_inst): obj_inst.tools = deepcopy(tools) obj_inst.create_geometry() obj_inst.source_file = app_inst.f_handlers.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst, filename=None, use_thread=False) ret_val = self.app.app_obj.new_object("excellon", _("Alignment Drills"), obj_init) self.drill_values = '' if not ret_val == 'fail': self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created...")) def on_pick_hole(self): # get the Excellon file whose geometry will contain the desired drill hole selection_index = self.ui.exc_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.ui.exc_combo.rootModelIndex()) try: self.exc_hole_obj = model_index.internalPointer().obj except Exception: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) return # disengage the grid snapping since it will be hard to find the drills or pads on grid if self.app.ui.grid_snap_btn.isChecked(): self.grid_status_memory = True self.app.ui.grid_snap_btn.trigger() else: self.grid_status_memory = False self.local_connected = True # disable the Notebook while in this feature self.app.ui.notebook.setDisabled(True) self.app.call_source = "2_sided_tool" self.app.inform.emit('%s.' % _("Click on canvas within the desired Excellon drill hole")) self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release) if self.app.is_legacy is False: self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) else: self.canvas.graph_event_disconnect(self.app.mr) def on_mouse_click_release(self, event): if self.app.is_legacy is False: 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 pos_canvas = self.canvas.translate_coords(event_pos) if event.button == 1: click_pt = Point([pos_canvas[0], pos_canvas[1]]) if self.app.selection_type is not None: # delete previous selection shape self.app.delete_selection_shape() self.app.selection_type = None else: if self.exc_hole_obj.kind.lower() == 'excellon': for tool, tool_dict in self.exc_hole_obj.tools.items(): for geo in tool_dict['solid_geometry']: if click_pt.within(geo): center_pt = geo.centroid center_pt_coords = ( self.app.dec_format(center_pt.x, self.decimals), self.app.dec_format(center_pt.y, self.decimals) ) self.app.delete_selection_shape() self.ui.axis_location.set_value('point') # set the reference point for mirror self.ui.point_entry.set_value(center_pt_coords) self.on_exit() self.app.inform.emit('[success] %s' % _("Mirror reference point set.")) elif event.button == right_button and self.app.event_is_dragging is False: self.on_exit(cancelled=True) def on_exit(self, cancelled=None): self.app.call_source = "app" self.app.ui.notebook.setDisabled(False) if cancelled is True: self.app.delete_selection_shape() self.disconnect_events() self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request.")) def disconnect_events(self): self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) if self.app.is_legacy is False: self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) else: self.canvas.graph_event_disconnect(self.mr) self.local_connected = False def on_mirror(self): selection_index = self.ui.object_combo.currentIndex() # fcobj = self.app.collection.object_list[selection_index] model_index = self.app.collection.index(selection_index, 0, self.ui.object_combo.rootModelIndex()) try: fcobj = model_index.internalPointer().obj except Exception: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return if fcobj.kind not in ['gerber', 'geometry', 'excellon']: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) return axis = self.ui.mirror_axis.get_value() mode = self.ui.axis_location.get_value() if mode == "box": selection_index_box = self.ui.box_combo.currentIndex() model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex()) try: bb_obj = model_index_box.internalPointer().obj except Exception: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) return xmin, ymin, xmax, ymax = bb_obj.bounds() px = 0.5 * (xmin + xmax) py = 0.5 * (ymin + ymax) else: try: px, py = self.ui.point_entry.get_value() except TypeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. " "Add coords and try again ...")) return fcobj.mirror(axis, [px, py]) self.app.app_obj.object_changed.emit(fcobj) fcobj.plot() self.app.inform.emit('[success] %s: %s' % (_("Object was mirrored"), str(fcobj.options['name']))) def on_point_add(self): val = self.app.defaults["global_point_clipboard_format"] % \ (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1]) self.ui.point_entry.set_value(val) def on_drill_add(self): self.drill_values += (self.app.defaults["global_point_clipboard_format"] % (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ',' self.ui.alignment_holes.set_value(self.drill_values) def on_drill_delete_last(self): drill_values_without_last_tupple = self.drill_values.rpartition('(')[0] self.drill_values = drill_values_without_last_tupple self.ui.alignment_holes.set_value(self.drill_values) def on_toggle_pointbox(self): val = self.ui.axis_location.get_value() if val == "point": self.ui.point_entry.show() self.ui.add_point_button.show() self.ui.box_type_radio.hide() self.ui.box_combo.hide() self.ui.exc_hole_lbl.hide() self.ui.exc_combo.hide() self.ui.pick_hole_button.hide() self.ui.align_ref_label_val.set_value(self.ui.point_entry.get_value()) elif val == 'box': self.ui.point_entry.hide() self.ui.add_point_button.hide() self.ui.box_type_radio.show() self.ui.box_combo.show() self.ui.exc_hole_lbl.hide() self.ui.exc_combo.hide() self.ui.pick_hole_button.hide() self.ui.align_ref_label_val.set_value("Box centroid") elif val == 'hole': self.ui.point_entry.show() self.ui.add_point_button.hide() self.ui.box_type_radio.hide() self.ui.box_combo.hide() self.ui.exc_hole_lbl.show() self.ui.exc_combo.show() self.ui.pick_hole_button.show() def on_bbox_coordinates(self): xmin = Inf ymin = Inf xmax = -Inf ymax = -Inf obj_list = self.app.collection.get_selected() if not obj_list: self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected."))) return for obj in obj_list: try: gxmin, gymin, gxmax, gymax = obj.bounds() xmin = min([xmin, gxmin]) ymin = min([ymin, gymin]) xmax = max([xmax, gxmax]) ymax = max([ymax, gymax]) except Exception as e: log.error("Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e)) self.ui.xmin_entry.set_value(xmin) self.ui.ymin_entry.set_value(ymin) self.ui.xmax_entry.set_value(xmax) self.ui.ymax_entry.set_value(ymax) cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin)) cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin)) val_txt = '(%s, %s)' % (cx, cy) self.ui.center_entry.set_value(val_txt) self.ui.axis_location.set_value('point') self.ui.point_entry.set_value(val_txt) self.app.delete_selection_shape() def on_xmin_clicked(self): xmin = self.ui.xmin_entry.get_value() self.ui.axis_location.set_value('point') try: px, py = self.ui.point_entry.get_value() val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py) except TypeError: val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0) self.ui.point_entry.set_value(val) def on_ymin_clicked(self): ymin = self.ui.ymin_entry.get_value() self.ui.axis_location.set_value('point') try: px, py = self.ui.point_entry.get_value() val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin) except TypeError: val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin) self.ui.point_entry.set_value(val) def on_xmax_clicked(self): xmax = self.ui.xmax_entry.get_value() self.ui.axis_location.set_value('point') try: px, py = self.ui.point_entry.get_value() val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py) except TypeError: val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0) self.ui.point_entry.set_value(val) def on_ymax_clicked(self): ymax = self.ui.ymax_entry.get_value() self.ui.axis_location.set_value('point') try: px, py = self.ui.point_entry.get_value() val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax) except TypeError: val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax) self.ui.point_entry.set_value(val) def reset_fields(self): self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ui.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ui.object_combo.setCurrentIndex(0) self.ui.box_combo.setCurrentIndex(0) self.ui.box_type_radio.set_value('grb') self.drill_values = "" self.ui.align_ref_label_val.set_value('') class DsidedUI: pluginName = _("2-Sided") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.layout = layout self.tools_frame = QtWidgets.QFrame() self.tools_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tools_frame) self.tools_box = QtWidgets.QVBoxLayout() self.tools_box.setContentsMargins(0, 0, 0, 0) self.tools_frame.setLayout(self.tools_box) self.title_box = QtWidgets.QHBoxLayout() self.tools_box.addLayout(self.title_box) # ## Title title_label = FCLabel("%s" % self.pluginName) title_label.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; } """) title_label.setToolTip( _("Create a Geometry object with\n" "toolpaths to cover the space outside the copper pattern.") ) self.title_box.addWidget(title_label) # App Level label self.level = QtWidgets.QToolButton() self.level.setToolTip( _( "Beginner Mode - many parameters are hidden.\n" "Advanced Mode - full control.\n" "Permanent change is done in 'Preferences' menu." ) ) # self.level.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) self.level.setCheckable(True) self.title_box.addWidget(self.level) # ############################################################################################################# # Source Object # ############################################################################################################# # Objects to be mirrored self.m_objects_label = FCLabel('%s' % _("Source Object")) self.m_objects_label.setToolTip('%s.' % _("Objects to be mirrored")) self.tools_box.addWidget(self.m_objects_label) source_frame = FCFrame() self.tools_box.addWidget(source_frame) # ## Grid Layout grid0 = FCGridLayout(v_spacing=5, h_spacing=3) grid0.setColumnStretch(0, 1) grid0.setColumnStretch(1, 0) source_frame.setLayout(grid0) # Type of object to be cutout self.type_obj_combo_label = FCLabel('%s:' % _("Type")) self.type_obj_combo_label.setToolTip( _("Select the type of application object to be processed in this tool.") ) self.object_type_radio = RadioSet([ {"label": _("Gerber"), "value": "grb"}, {"label": _("Excellon"), "value": "exc"}, {"label": _("Geometry"), "value": "geo"} ]) grid0.addWidget(self.type_obj_combo_label, 0, 0) grid0.addWidget(self.object_type_radio, 0, 1) # ## Gerber Object to mirror self.object_combo = FCComboBox() self.object_combo.setModel(self.app.collection) self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.object_combo.is_last = True grid0.addWidget(self.object_combo, 2, 0, 1, 2) # separator_line = QtWidgets.QFrame() # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # grid0.addWidget(separator_line, 7, 0, 1, 2) # ############################################################################################################# # ########## BOUNDS OPERATION ########################################################################### # ############################################################################################################# # ## Title Bounds Values self.bv_label = FCLabel('%s' % _('Bounds Values')) self.bv_label.setToolTip( _("Select on canvas the object(s)\n" "for which to calculate bounds values.") ) self.tools_box.addWidget(self.bv_label) self.bounds_frame = FCFrame() self.tools_box.addWidget(self.bounds_frame) grid1 = FCGridLayout(v_spacing=5, h_spacing=3) grid1.setColumnStretch(0, 0) grid1.setColumnStretch(1, 1) self.bounds_frame.setLayout(grid1) # Xmin value self.xmin_entry = FCDoubleSpinner(callback=self.confirmation_message) self.xmin_entry.set_precision(self.decimals) self.xmin_entry.set_range(-10000.0000, 10000.0000) self.xmin_btn = FCButton('%s:' % _("X min")) self.xmin_btn.setToolTip( _("Minimum location.") ) self.xmin_entry.setReadOnly(True) grid1.addWidget(self.xmin_btn, 0, 0) grid1.addWidget(self.xmin_entry, 0, 1) # Ymin value self.ymin_entry = FCDoubleSpinner(callback=self.confirmation_message) self.ymin_entry.set_precision(self.decimals) self.ymin_entry.set_range(-10000.0000, 10000.0000) self.ymin_btn = FCButton('%s:' % _("Y min")) self.ymin_btn.setToolTip( _("Minimum location.") ) self.ymin_entry.setReadOnly(True) grid1.addWidget(self.ymin_btn, 2, 0) grid1.addWidget(self.ymin_entry, 2, 1) # Xmax value self.xmax_entry = FCDoubleSpinner(callback=self.confirmation_message) self.xmax_entry.set_precision(self.decimals) self.xmax_entry.set_range(-10000.0000, 10000.0000) self.xmax_btn = FCButton('%s:' % _("X max")) self.xmax_btn.setToolTip( _("Maximum location.") ) self.xmax_entry.setReadOnly(True) grid1.addWidget(self.xmax_btn, 4, 0) grid1.addWidget(self.xmax_entry, 4, 1) # Ymax value self.ymax_entry = FCDoubleSpinner(callback=self.confirmation_message) self.ymax_entry.set_precision(self.decimals) self.ymax_entry.set_range(-10000.0000, 10000.0000) self.ymax_btn = FCButton('%s:' % _("Y max")) self.ymax_btn.setToolTip( _("Maximum location.") ) self.ymax_entry.setReadOnly(True) grid1.addWidget(self.ymax_btn, 6, 0) grid1.addWidget(self.ymax_entry, 6, 1) # Center point value self.center_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.center_entry.setPlaceholderText(_("Center point coordinates")) self.center_btn = FCButton('%s:' % _("Centroid")) self.center_btn.setToolTip( _("The center point location for the rectangular\n" "bounding shape. Centroid. Format is (x, y).") ) self.center_entry.setReadOnly(True) grid1.addWidget(self.center_btn, 8, 0) grid1.addWidget(self.center_entry, 8, 1) # Calculate Bounding box self.calculate_bb_button = FCButton(_("Calculate Bounds Values")) self.calculate_bb_button.setToolTip( _("Calculate the enveloping rectangular shape coordinates,\n" "for the selection of objects.\n" "The envelope shape is parallel with the X, Y axis.") ) self.calculate_bb_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.calculate_bb_button) # ############################################################################################################# # ########## MIRROR OPERATION ########################################################################### # ############################################################################################################# self.param_label = FCLabel('%s' % _("Mirror Operation")) self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation")) self.tools_box.addWidget(self.param_label) mirror_frame = FCFrame() self.tools_box.addWidget(mirror_frame) grid2 = FCGridLayout(v_spacing=5, h_spacing=3) grid2.setColumnStretch(0, 0) grid2.setColumnStretch(1, 1) mirror_frame.setLayout(grid2) # ## Axis self.mirax_label = FCLabel('%s:' % _("Axis")) self.mirax_label.setToolTip(_("Mirror vertically (X) or horizontally (Y).")) self.mirror_axis = RadioSet( [ {'label': 'X', 'value': 'X'}, {'label': 'Y', 'value': 'Y'} ], orientation='vertical', stretch=False ) grid2.addWidget(self.mirax_label, 2, 0) grid2.addWidget(self.mirror_axis, 2, 1, 1, 2) # ## Axis Location self.axloc_label = FCLabel('%s:' % _("Reference")) self.axloc_label.setToolTip( _("The coordinates used as reference for the mirror operation.\n" "Can be:\n" "- Point -> a set of coordinates (x,y) around which the object is mirrored\n" "- Box -> a set of coordinates (x, y) obtained from the center of the\n" "bounding box of another object selected below\n" "- Hole Snap -> a point defined by the center of a drill hole in a Excellon object") ) self.axis_location = RadioSet( [ {'label': _('Point'), 'value': 'point'}, {'label': _('Box'), 'value': 'box'}, {'label': _('Hole Snap'), 'value': 'hole'}, ] ) grid2.addWidget(self.axloc_label, 4, 0) grid2.addWidget(self.axis_location, 4, 1, 1, 2) # ## Point/Box self.point_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.point_entry.setPlaceholderText(_("Point coordinates")) # Add a reference self.add_point_button = FCButton(_("Add")) self.add_point_button.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png')) self.add_point_button.setToolTip( _("Add the coordinates in format (x, y) through which the mirroring axis\n " "selected in 'MIRROR AXIS' pass.\n" "The (x, y) coordinates are captured by pressing SHIFT key\n" "and left mouse button click on canvas or you can enter the coordinates manually.") ) # self.add_point_button.setStyleSheet(""" # QPushButton # { # font-weight: bold; # } # """) self.add_point_button.setMinimumWidth(60) grid2.addWidget(self.point_entry, 7, 0, 1, 2) grid2.addWidget(self.add_point_button, 7, 2) self.exc_hole_lbl = FCLabel('%s:' % _("Excellon")) self.exc_hole_lbl.setToolTip( _("Object that holds holes that can be picked as reference for mirroring.") ) # Excellon Object that holds the holes self.exc_combo = FCComboBox() self.exc_combo.setModel(self.app.collection) self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) self.exc_combo.is_last = True self.exc_hole_lbl.hide() self.exc_combo.hide() grid2.addWidget(self.exc_hole_lbl, 10, 0) grid2.addWidget(self.exc_combo, 10, 1, 1, 2) self.pick_hole_button = FCButton(_("Pick hole")) self.pick_hole_button.setToolTip( _("Click inside a drill hole that belong to the selected Excellon object,\n" "and the hole center coordinates will be copied to the Point field.") ) self.pick_hole_button.hide() grid2.addWidget(self.pick_hole_button, 12, 0, 1, 3) # ## Grid Layout grid_lay3 = FCGridLayout(v_spacing=5, h_spacing=3) grid_lay3.setColumnStretch(0, 0) grid_lay3.setColumnStretch(1, 1) grid2.addLayout(grid_lay3, 14, 0, 1, 3) # Type of object used as BOX reference self.box_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'grb'}, {'label': _('Excellon'), 'value': 'exc'}, {'label': _('Geometry'), 'value': 'geo'}]) self.box_type_radio.setToolTip( _("It can be of type: Gerber or Excellon or Geometry.\n" "The coordinates of the center of the bounding box are used\n" "as reference for mirror operation.") ) self.box_type_radio.hide() grid_lay3.addWidget(self.box_type_radio, 1, 0, 1, 2) # Object used as BOX reference self.box_combo = FCComboBox() self.box_combo.setModel(self.app.collection) self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.box_combo.is_last = True self.box_combo.hide() grid_lay3.addWidget(self.box_combo, 3, 0, 1, 2) # Mirror Button self.mirror_button = FCButton(_("Mirror")) self.mirror_button.setIcon(QtGui.QIcon(self.app.resource_location + '/doubleside16.png')) self.mirror_button.setToolTip( _("Mirrors (flips) the specified object around \n" "the specified axis. Does not create a new \n" "object, but modifies it.") ) self.mirror_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.mirror_button) # ############################################################################################################# # ########## ALIGNMENT OPERATION ######################################################################## # ############################################################################################################# # ## Alignment holes self.alignment_label = FCLabel('%s' % _('PCB Alignment')) self.alignment_label.setToolTip( _("Creates an Excellon Object containing the\n" "specified alignment holes and their mirror\n" "images.") ) self.tools_box.addWidget(self.alignment_label) align_frame = FCFrame() self.tools_box.addWidget(align_frame) grid4 = FCGridLayout(v_spacing=5, h_spacing=3) grid4.setColumnStretch(0, 0) grid4.setColumnStretch(1, 1) align_frame.setLayout(grid4) # ## Drill diameter for alignment holes self.dt_label = FCLabel("%s:" % _('Drill Dia')) self.dt_label.setToolTip( _("Diameter of the drill for the alignment holes.") ) self.drill_dia = FCDoubleSpinner(callback=self.confirmation_message) self.drill_dia.setToolTip( _("Diameter of the drill for the alignment holes.") ) self.drill_dia.set_precision(self.decimals) self.drill_dia.set_range(0.0000, 10000.0000) grid4.addWidget(self.dt_label, 2, 0) grid4.addWidget(self.drill_dia, 2, 1) # ## Alignment Axis self.align_ax_label = FCLabel('%s:' % _("Axis")) self.align_ax_label.setToolTip( _("Mirror vertically (X) or horizontally (Y).") ) self.align_axis_radio = RadioSet( [ {'label': 'X', 'value': 'X'}, {'label': 'Y', 'value': 'Y'} ], orientation='vertical', stretch=False ) grid4.addWidget(self.align_ax_label, 4, 0) grid4.addWidget(self.align_axis_radio, 4, 1) # ## Alignment Reference Point self.align_ref_label = FCLabel('%s:' % _("Reference")) self.align_ref_label.setToolTip( _("The reference point used to create the second alignment drill\n" "from the first alignment drill, by doing mirror.\n" "It can be modified in the Mirror Parameters -> Reference section") ) self.align_ref_label_val = NumericalEvalTupleEntry(border_color='#0069A9') self.align_ref_label_val.setToolTip( _("The reference point used to create the second alignment drill\n" "from the first alignment drill, by doing mirror.\n" "It can be modified in the Mirror Parameters -> Reference section") ) self.align_ref_label_val.setDisabled(True) grid4.addWidget(self.align_ref_label, 6, 0) grid4.addWidget(self.align_ref_label_val, 6, 1) # ## Alignment holes self.ah_label = FCLabel("%s:" % _('Alignment Drill Coordinates')) self.ah_label.setToolTip( _("Alignment holes (x1, y1), (x2, y2), ... " "on one side of the mirror axis. For each set of (x, y) coordinates\n" "entered here, a pair of drills will be created:\n\n" "- one drill at the coordinates from the field\n" "- one drill in mirror position over the axis selected above in the 'Align Axis'.") ) self.alignment_holes = NumericalEvalTupleEntry(border_color='#0069A9') self.alignment_holes.setPlaceholderText(_("Drill coordinates")) grid4.addWidget(self.ah_label, 8, 0, 1, 2) grid4.addWidget(self.alignment_holes, 9, 0, 1, 2) self.add_drill_point_button = FCButton(_("Add")) self.add_drill_point_button.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png')) self.add_drill_point_button.setToolTip( _("Add alignment drill holes coordinates in the format: (x1, y1), (x2, y2), ... \n" "on one side of the alignment axis.\n\n" "The coordinates set can be obtained:\n" "- press SHIFT key and left mouse clicking on canvas. Then click Add.\n" "- press SHIFT key and left mouse clicking on canvas. Then Ctrl+V in the field.\n" "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n" "- by entering the coords manually in the format: (x1, y1), (x2, y2), ...") ) # self.add_drill_point_button.setStyleSheet(""" # QPushButton # { # font-weight: bold; # } # """) self.delete_drill_point_button = FCButton(_("Delete Last")) self.delete_drill_point_button.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png')) self.delete_drill_point_button.setToolTip( _("Delete the last coordinates tuple in the list.") ) drill_hlay = QtWidgets.QHBoxLayout() drill_hlay.addWidget(self.add_drill_point_button) drill_hlay.addWidget(self.delete_drill_point_button) grid4.addLayout(drill_hlay, 10, 0, 1, 2) # ## Buttons self.create_excellon_button = FCButton(_("Create Excellon Object")) self.create_excellon_button.setIcon(QtGui.QIcon(self.app.resource_location + '/drill32.png')) self.create_excellon_button.setToolTip( _("Creates an Excellon Object containing the\n" "specified alignment holes and their mirror\n" "images.") ) self.create_excellon_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.tools_box.addWidget(self.create_excellon_button) self.tools_box.addStretch(1) # ## Reset Tool self.reset_button = FCButton(_("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.tools_box.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)