# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 3/10/2019 # # MIT Licence # # ########################################################## from PyQt6 import QtWidgets, QtCore from appTool import AppTool from appGUI.VisPyVisuals import ShapeCollection import logging from copy import copy from shapely import Polygon import gettext import appTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class ToolMove(AppTool): pluginName = _("Move") replot_signal = QtCore.pyqtSignal(list) def __init__(self, app): AppTool.__init__(self, app) self.app = app self.decimals = self.app.decimals self.layout.setContentsMargins(0, 0, 3, 0) self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Maximum) self.clicked_move = 0 self.point1 = None self.point2 = None # the default state is disabled for the Move command self.setVisible(False) self.sel_rect = None self.old_coords = [] self.sel_shapes = self.app.sel_shapes self.mm = None self.mp = None self.kr = None self.replot_signal[list].connect(self.replot) def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='M', **kwargs) def run(self, toggle): self.app.defaults.report_usage("ToolMove()") if self.app.plugin_tab_locked is True: return self.toggle() def toggle(self, toggle=False): if self.isVisible(): self.setVisible(False) if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_move) self.app.plotcanvas.graph_event_disconnect('mouse_press', self.on_left_click) self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_press) self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) else: self.app.plotcanvas.graph_event_disconnect(self.mm) self.app.plotcanvas.graph_event_disconnect(self.mp) self.app.plotcanvas.graph_event_disconnect(self.kr) self.app.kr = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) self.clicked_move = 0 # signal that there is no command active self.app.command_active = None # delete the selection box self.delete_shape() return else: self.setVisible(True) # signal that there is a command active, and it is 'Move' self.app.command_active = "Move" sel_obj_list = self.app.collection.get_selected() if sel_obj_list: self.app.inform.emit(_("MOVE: Click on the Start point ...")) # if we have an object selected then we can safely activate the mouse events self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_move) self.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.on_left_click) self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_press) # draw the selection box self.draw_sel_bbox() else: self.toggle() self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. No object(s) to move.")) def on_left_click(self, event): # mouse click will be accepted only if the left button is clicked # this is necessary because right mouse click and middle mouse click # are used for panning on the canvas event_pos = event.pos if self.app.use_3d_engine else (event.xdata, event.ydata) if event.button == 1: if self.clicked_move == 0: pos_canvas = self.app.plotcanvas.translate_coords(event_pos) # if GRID is active we need to get the snapped positions if self.app.grid_status(): pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) else: pos = pos_canvas if self.point1 is None: self.point1 = pos else: self.point2 = copy(self.point1) self.point1 = pos self.app.inform.emit(_("Click on the DESTINATION point ...")) if self.clicked_move == 1: try: pos_canvas = self.app.plotcanvas.translate_coords(event_pos) # delete the selection bounding box self.delete_shape() # if GRID is active we need to get the snapped positions if self.app.grid_status(): pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) else: pos = pos_canvas dx = pos[0] - self.point1[0] dy = pos[1] - self.point1[1] self.move_handler(offset=(dx, dy)) self.clicked_move = 0 self.toggle() return except TypeError as e: self.app.log.debug("ToolMove.on_left_click() --> %s" % str(e)) self.app.inform.emit('[ERROR_NOTCL] ToolMove. %s' % _('Error when mouse left click.')) return self.clicked_move = 1 def replot(self, obj_list): def worker_task(): with self.app.proc_container.new('%s ...' % _("Plotting")): for sel_obj in obj_list: sel_obj.plot() self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def move_handler(self, offset, objects=None): """ Actual move is done here. :param offset: How much to move objects on both directions :type offset: tuple :param objects: objects to move :type objects: [list, None] :return: :rtype: """ dx, dy = offset if not objects: # move only the objects selected and plotted and visible obj_list = [ obj for obj in self.app.collection.get_selected() if obj.obj_options['plot'] and obj.visible is True ] else: obj_list = objects def job_move(app_obj): with self.app.proc_container.new('%s...' % _("Moving")): if not obj_list: app_obj.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected."))) return "fail" try: # remove any mark aperture shape that may be displayed for sel_obj in obj_list: # if the Gerber mark shapes are enabled they need to be disabled before move if sel_obj.kind == 'gerber': sel_obj.ui.aperture_table_visibility_cb.setChecked(False) try: sel_obj.replotApertures.emit() except Exception: pass # offset solid_geometry sel_obj.offset((dx, dy)) # Update the object bounding box options a, b, c, d = sel_obj.bounds() sel_obj.obj_options['xmin'] = a sel_obj.obj_options['ymin'] = b sel_obj.obj_options['xmax'] = c sel_obj.obj_options['ymax'] = d try: sel_obj.set_offset_values() except AttributeError: # not all objects have this method pass # update the source_file with the new positions for sel_obj in obj_list: out_name = sel_obj.obj_options["name"] if sel_obj.kind == 'gerber': sel_obj.source_file = self.app.f_handlers.export_gerber( obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) elif sel_obj.kind == 'excellon': sel_obj.source_file = self.app.f_handlers.export_excellon( obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False) except Exception as err: app_obj.log.error('[ERROR_NOTCL] %s --> %s' % ('ToolMove.move_handler()', str(err))) return "fail" # time to plot the moved objects app_obj.replot_signal.emit(obj_list) # delete the selection bounding box self.delete_shape() self.app.inform.emit('[success] %s' % _("Done.")) self.app.worker_task.emit({'fcn': job_move, 'params': [self]}) def on_move(self, event): event_pos = event.pos if self.app.use_3d_engine else (event.xdata, event.ydata) try: x = float(event_pos[0]) y = float(event_pos[1]) except TypeError: return pos_canvas = self.app.plotcanvas.translate_coords((x, y)) # if GRID is active we need to get the snapped positions if self.app.grid_status(): pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) else: pos = pos_canvas if self.point1 is None: dx = pos[0] dy = pos[1] else: dx = pos[0] - self.point1[0] dy = pos[1] - self.point1[1] if self.clicked_move == 1: self.update_sel_bbox((dx, dy)) def on_key_press(self, event): if event.key == 'escape': # abort the move action self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) self.toggle() return def draw_sel_bbox(self): xminlist = [] yminlist = [] xmaxlist = [] ymaxlist = [] obj_list = self.app.collection.get_selected() # first get a bounding box to fit all for obj in obj_list: # don't move disabled objects, move only plotted objects if obj.obj_options['plot']: xmin, ymin, xmax, ymax = obj.bounds() xminlist.append(xmin) yminlist.append(ymin) xmaxlist.append(xmax) ymaxlist.append(ymax) # get the minimum x,y and maximum x,y for all objects selected xminimal = min(xminlist) yminimal = min(yminlist) xmaximal = max(xmaxlist) ymaximal = max(ymaxlist) p1 = (xminimal, yminimal) p2 = (xmaximal, yminimal) p3 = (xmaximal, ymaximal) p4 = (xminimal, ymaximal) self.old_coords = [p1, p2, p3, p4] self.draw_shape(Polygon(self.old_coords)) if self.app.use_3d_engine: self.sel_shapes.redraw() def update_sel_bbox(self, pos): self.delete_shape() pt1 = (self.old_coords[0][0] + pos[0], self.old_coords[0][1] + pos[1]) pt2 = (self.old_coords[1][0] + pos[0], self.old_coords[1][1] + pos[1]) pt3 = (self.old_coords[2][0] + pos[0], self.old_coords[2][1] + pos[1]) pt4 = (self.old_coords[3][0] + pos[0], self.old_coords[3][1] + pos[1]) self.draw_shape(Polygon([pt1, pt2, pt3, pt4])) if self.app.use_3d_engine: self.sel_shapes.redraw() def delete_shape(self): self.sel_shapes.clear() self.sel_shapes.redraw() def draw_shape(self, shp): if self.app.app_units.upper() == 'MM': proc_shape = shp.buffer(-0.1) proc_shape = proc_shape.buffer(0.2) else: proc_shape = shp.buffer(-0.00393) proc_shape = proc_shape.buffer(0.00787) # face = Color('blue') # face.alpha = 0.2 if self.app.options['global_selection_shape_as_line'] is True: proc_shape = proc_shape.exterior face = '#0000FF' + str(hex(int(0.2 * 255)))[2:] outline = '#0000FFAF' self.sel_shapes.add(proc_shape, color=outline, face_color=face, update=True, layer=0, tolerance=None) # end of file