# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 1/13/2020 # # MIT Licence # # ########################################################## from PyQt5 import QtWidgets, QtCore, QtGui from appTool import AppTool from appGUI.GUIElements import FCComboBox, RadioSet, FCLabel, FCButton import math from shapely.geometry import Point from shapely.affinity import translate import gettext import appTranslation as fcTranslate import builtins import logging fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class AlignObjects(AppTool): toolName = _("Align Objects") def __init__(self, app): AppTool.__init__(self, app) self.app = app self.decimals = app.decimals self.canvas = self.app.plotcanvas # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = AlignUI(layout=self.layout, app=self.app) self.toolName = self.ui.toolName # Signals self.ui.align_object_button.clicked.connect(self.on_align) self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed) self.ui.type_aligner_obj_radio.activated_custom.connect(self.on_type_aligner_changed) self.ui.reset_button.clicked.connect(self.set_tool_ui) self.mr = None # if the mouse events are connected to a local method set this True self.local_connected = False # store the status of the grid self.grid_status_memory = None self.aligned_obj = None self.aligner_obj = None # this is one of the objects: self.aligned_obj or self.aligner_obj self.target_obj = None # here store the alignment points self.clicked_points = [] self.align_type = None # old colors of objects involved in the alignment self.aligner_old_fill_color = None self.aligner_old_line_color = None self.aligned_old_fill_color = None self.aligned_old_line_color = None def run(self, toggle=True): self.app.defaults.report_usage("ToolAlignObjects()") 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, _("Align Tool")) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+A', **kwargs) def set_tool_ui(self): self.reset_fields() self.clicked_points = [] self.target_obj = None self.aligned_obj = None self.aligner_obj = None self.aligner_old_fill_color = None self.aligner_old_line_color = None self.aligned_old_fill_color = None self.aligned_old_line_color = None self.ui.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"]) self.ui.type_obj_radio.set_value('grb') self.ui.type_aligner_obj_radio.set_value('grb') if self.local_connected is True: self.disconnect_cal_events() def on_type_obj_changed(self, val): obj_type = {'grb': 0, 'exc': 1}[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"}[val] def on_type_aligner_changed(self, val): obj_type = {'grb': 0, 'exc': 1}[val] self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.aligner_object_combo.setCurrentIndex(0) self.ui.aligner_object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val] def on_align(self): self.app.delete_selection_shape() obj_sel_index = self.ui.object_combo.currentIndex() obj_model_index = self.app.collection.index(obj_sel_index, 0, self.ui.object_combo.rootModelIndex()) try: self.aligned_obj = obj_model_index.internalPointer().obj except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected...")) return aligner_obj_sel_index = self.ui.aligner_object_combo.currentIndex() aligner_obj_model_index = self.app.collection.index( aligner_obj_sel_index, 0, self.ui.aligner_object_combo.rootModelIndex()) try: self.aligner_obj = aligner_obj_model_index.internalPointer().obj except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected...")) return self.align_type = self.ui.a_type_radio.get_value() # 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 self.aligner_old_fill_color = self.aligner_obj.fill_color self.aligner_old_line_color = self.aligner_obj.outline_color self.aligned_old_fill_color = self.aligned_obj.fill_color self.aligned_old_line_color = self.aligned_obj.outline_color self.target_obj = self.aligned_obj self.set_color() self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point."))) 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.target_obj.kind.lower() == 'excellon': for tool, tool_dict in self.target_obj.tools.items(): for geo in tool_dict['solid_geometry']: if click_pt.within(geo): center_pt = geo.centroid self.clicked_points.append( [ float('%.*f' % (self.decimals, center_pt.x)), float('%.*f' % (self.decimals, center_pt.y)) ] ) self.check_points() elif self.target_obj.kind.lower() == 'gerber': for apid, apid_val in self.target_obj.apertures.items(): for geo_el in apid_val['geometry']: if 'solid' in geo_el: if click_pt.within(geo_el['solid']): if isinstance(geo_el['follow'], Point): center_pt = geo_el['solid'].centroid self.clicked_points.append( [ float('%.*f' % (self.decimals, center_pt.x)), float('%.*f' % (self.decimals, center_pt.y)) ] ) self.check_points() elif event.button == right_button and self.app.event_is_dragging is False: self.reset_color() self.clicked_points = [] self.disconnect_cal_events() self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request.")) def check_points(self): if len(self.clicked_points) == 1: self.app.inform.emit('%s: %s. %s' % ( _("First Point"), _("Click on the DESTINATION point ..."), _("Or right click to cancel."))) self.target_obj = self.aligner_obj self.reset_color() self.set_color() if len(self.clicked_points) == 2: if self.align_type == 'sp': self.align_translate() self.app.inform.emit('[success] %s' % _("Done.")) self.app.plot_all() self.disconnect_cal_events() return else: self.app.inform.emit('%s: %s. %s' % ( _("Second Point"), _("Click on the START point."), _("Or right click to cancel."))) self.target_obj = self.aligned_obj self.reset_color() self.set_color() if len(self.clicked_points) == 3: self.app.inform.emit('%s: %s. %s' % ( _("Second Point"), _("Click on the DESTINATION point ..."), _("Or right click to cancel."))) self.target_obj = self.aligner_obj self.reset_color() self.set_color() if len(self.clicked_points) == 4: self.align_translate() self.align_rotate() self.app.inform.emit('[success] %s' % _("Done.")) self.disconnect_cal_events() self.app.plot_all() def align_translate(self): dx = self.clicked_points[1][0] - self.clicked_points[0][0] dy = self.clicked_points[1][1] - self.clicked_points[0][1] self.aligned_obj.offset((dx, dy)) # Update the object bounding box options a, b, c, d = self.aligned_obj.bounds() self.aligned_obj.options['xmin'] = a self.aligned_obj.options['ymin'] = b self.aligned_obj.options['xmax'] = c self.aligned_obj.options['ymax'] = d def align_rotate(self): dx = self.clicked_points[1][0] - self.clicked_points[0][0] dy = self.clicked_points[1][1] - self.clicked_points[0][1] test_rotation_pt = translate(Point(self.clicked_points[2]), xoff=dx, yoff=dy) new_start = (test_rotation_pt.x, test_rotation_pt.y) new_dest = self.clicked_points[3] origin_pt = self.clicked_points[1] dxd = new_dest[0] - origin_pt[0] dyd = new_dest[1] - origin_pt[1] dxs = new_start[0] - origin_pt[0] dys = new_start[1] - origin_pt[1] rotation_not_needed = (abs(new_start[0] - new_dest[0]) <= (10 ** -self.decimals)) or \ (abs(new_start[1] - new_dest[1]) <= (10 ** -self.decimals)) if rotation_not_needed is False: # calculate rotation angle angle_dest = math.degrees(math.atan(dyd / dxd)) angle_start = math.degrees(math.atan(dys / dxs)) angle = angle_dest - angle_start self.aligned_obj.rotate(angle=angle, point=origin_pt) def disconnect_cal_events(self): # restore the Grid snapping if it was active before if self.grid_status_memory is True: self.app.ui.grid_snap_btn.trigger() 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 self.aligner_old_fill_color = None self.aligner_old_line_color = None self.aligned_old_fill_color = None self.aligned_old_line_color = None def set_color(self): new_color = "#15678abf" new_line_color = new_color self.target_obj.shapes.redraw( update_colors=(new_color, new_line_color) ) def reset_color(self): self.aligned_obj.shapes.redraw( update_colors=(self.aligned_old_fill_color, self.aligned_old_line_color) ) self.aligner_obj.shapes.redraw( update_colors=(self.aligner_old_fill_color, self.aligner_old_line_color) ) def reset_fields(self): self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) class AlignUI: toolName = _("Align Objects") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals 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(QtWidgets.QLabel("")) # Form Layout grid0 = QtWidgets.QGridLayout() grid0.setColumnStretch(0, 0) grid0.setColumnStretch(1, 1) self.layout.addLayout(grid0) self.aligned_label = FCLabel('%s:' % _("MOVING object")) grid0.addWidget(self.aligned_label, 0, 0, 1, 2) self.aligned_label.setToolTip( _("Specify the type of object to be aligned.\n" "It can be of type: Gerber or Excellon.\n" "The selection here decide the type of objects that will be\n" "in the Object combobox.") ) # Type of object to be aligned self.type_obj_radio = RadioSet([ {"label": _("Gerber"), "value": "grb"}, {"label": _("Excellon"), "value": "exc"}, ]) grid0.addWidget(self.type_obj_radio, 3, 0, 1, 2) # Object to be aligned 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 self.object_combo.setToolTip( _("Object to be aligned.") ) grid0.addWidget(self.object_combo, 4, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 5, 0, 1, 2) grid0.addWidget(FCLabel(''), 6, 0, 1, 2) self.aligned_label = FCLabel('%s:' % _("DESTINATION object")) self.aligned_label.setToolTip( _("Specify the type of object to be aligned to.\n" "It can be of type: Gerber or Excellon.\n" "The selection here decide the type of objects that will be\n" "in the Object combobox.") ) grid0.addWidget(self.aligned_label, 7, 0, 1, 2) # Type of object to be aligned to = aligner self.type_aligner_obj_radio = RadioSet([ {"label": _("Gerber"), "value": "grb"}, {"label": _("Excellon"), "value": "exc"}, ]) grid0.addWidget(self.type_aligner_obj_radio, 8, 0, 1, 2) # Object to be aligned to = aligner self.aligner_object_combo = FCComboBox() self.aligner_object_combo.setModel(self.app.collection) self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.aligner_object_combo.is_last = True self.aligner_object_combo.setToolTip( _("Object to be aligned to. Aligner.") ) grid0.addWidget(self.aligner_object_combo, 9, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 10, 0, 1, 2) grid0.addWidget(QtWidgets.QLabel(''), 11, 0, 1, 2) # Alignment Type self.a_type_lbl = FCLabel('%s:' % _("Alignment Type")) self.a_type_lbl.setToolTip( _("The type of alignment can be:\n" "- Single Point -> it require a single point of sync, the action will be a translation\n" "- Dual Point -> it require two points of sync, the action will be translation followed by rotation") ) self.a_type_radio = RadioSet( [ {'label': _('Single Point'), 'value': 'sp'}, {'label': _('Dual Point'), 'value': 'dp'} ]) grid0.addWidget(self.a_type_lbl, 12, 0, 1, 2) grid0.addWidget(self.a_type_radio, 13, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 14, 0, 1, 2) # Buttons self.align_object_button = FCButton(_("Align Object")) self.align_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/align16.png')) self.align_object_button.setToolTip( _("Align the specified object to the aligner object.\n" "If only one point is used then it assumes translation.\n" "If tho points are used it assume translation and rotation.") ) self.align_object_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.layout.addWidget(self.align_object_button) self.layout.addStretch() # ## 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.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)