From 2bde43ca3f3dadffe095c6c52683ef4006a7dfb3 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Fri, 25 Oct 2019 19:52:11 +0300 Subject: [PATCH] - added a new FlatCAM Tool: Copper Fill Tool. It will pour copper into a Gerber filling all empty space with copper, at a clearance distance of the Gerber features --- FlatCAMApp.py | 5 + README.md | 1 + camlib.py | 8 +- flatcamGUI/FlatCAMGUI.py | 7 + flatcamTools/ToolCopperFill.py | 678 +++++++++++++++++++++++++++++ flatcamTools/ToolNonCopperClear.py | 14 +- flatcamTools/ToolPaint.py | 3 +- flatcamTools/ToolQRCode.py | 10 +- flatcamTools/__init__.py | 2 + share/copperfill16.png | Bin 0 -> 569 bytes share/copperfill32.png | Bin 0 -> 958 bytes 11 files changed, 710 insertions(+), 18 deletions(-) create mode 100644 flatcamTools/ToolCopperFill.py create mode 100644 share/copperfill16.png create mode 100644 share/copperfill32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index bbc236d1..1cbb90dc 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2568,6 +2568,7 @@ class App(QtCore.QObject): self.pcb_wizard_tool = None self.cal_exc_tool = None self.qrcode_tool = None + self.copperfill_tool = None # always install tools only after the shell is initialized because the self.inform.emit() depends on shell self.install_tools() @@ -3154,6 +3155,9 @@ class App(QtCore.QObject): self.paint_tool.install(icon=QtGui.QIcon('share/paint16.png'), pos=self.ui.menutool, before=self.sub_tool.menuAction, separator=True) + self.copperfill_tool = ToolCopperFill(self) + self.copperfill_tool.install(icon=QtGui.QIcon('share/copperfill32.png'), pos=self.ui.menutool) + self.qrcode_tool = QRCode(self) self.qrcode_tool.install(icon=QtGui.QIcon('share/qrcode32.png'), pos=self.ui.menutool) @@ -3281,6 +3285,7 @@ class App(QtCore.QObject): self.ui.calculators_btn.triggered.connect(lambda: self.calculator_tool.run(toggle=True)) self.ui.transform_btn.triggered.connect(lambda: self.transform_tool.run(toggle=True)) self.ui.qrcode_btn.triggered.connect(lambda: self.qrcode_tool.run(toggle=True)) + self.ui.copperfill_btn.triggered.connect(lambda: self.copperfill_tool.run(toggle=True)) def object2editor(self): """ diff --git a/README.md b/README.md index bad77d5f..07404fb2 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ CAD program, and create G-Code for Isolation routing. - QRCode Tool: added the feature to save the PNG file with transparent background - QRCode Tool: added GUI category in Preferences window - QRCode Tool: shortcut key for this tool is now ALT+Q while PDF import Tool was relegated to CTRL+Q combo key shortcut +- added a new FlatCAM Tool: Copper Fill Tool. It will pour copper into a Gerber filling all empty space with copper, at a clearance distance of the Gerber features 24.10.2019 diff --git a/camlib.py b/camlib.py index 3fd52d13..adb41c0e 100644 --- a/camlib.py +++ b/camlib.py @@ -930,10 +930,10 @@ class Geometry(object): if follow: geo_iso = self.follow_geometry else: - if isinstance(self.solid_geometry, list): - temp_geo = cascaded_union(self.solid_geometry) - else: - temp_geo = self.solid_geometry + # if isinstance(self.solid_geometry, list): + # temp_geo = cascaded_union(self.solid_geometry) + # else: + # temp_geo = self.solid_geometry # Remember: do not make a buffer for each element in the solid_geometry because it will cut into # other copper features diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index d98dcf7d..43c1f3be 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -756,6 +756,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'), _("Calculators Tool")) self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool")) self.qrcode_btn = self.toolbartools.addAction(QtGui.QIcon('share/qrcode32.png'), _("QRCode Tool")) + self.copperfill_btn = self.toolbartools.addAction(QtGui.QIcon('share/copperfill32.png'), _("Copper Fill Tool")) # ######################################################################## # ########################## Excellon Editor Toolbar# #################### @@ -2191,6 +2192,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): _("Calculators Tool")) self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool")) self.qrcode_btn = self.toolbartools.addAction(QtGui.QIcon('share/qrcode32.png'), _("QRCode Tool")) + self.copperfill_btn = self.toolbartools.addAction(QtGui.QIcon('share/copperfill32.png'), _("Copper Fill Tool")) # ## Excellon Editor Toolbar # ## self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select")) @@ -2506,6 +2508,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.cal_exc_tool.run(toggle=True) return + # Copper Fill Tool + if key == QtCore.Qt.Key_F: + self.app.copperfill_tool.run(toggle=True) + return + # Toggle Grid lines if key == QtCore.Qt.Key_G: self.app.on_toggle_grid_lines() diff --git a/flatcamTools/ToolCopperFill.py b/flatcamTools/ToolCopperFill.py new file mode 100644 index 00000000..2756f16d --- /dev/null +++ b/flatcamTools/ToolCopperFill.py @@ -0,0 +1,678 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# File Author: Marius Adrian Stanciu (c) # +# Date: 10/25/2019 # +# MIT Licence # +# ########################################################## + +from PyQt5 import QtWidgets, QtCore + +import FlatCAMApp +from FlatCAMTool import FlatCAMTool +from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMExcellon + +import shapely.geometry.base as base +from shapely.ops import cascaded_union, unary_union +from shapely.geometry import Polygon, MultiPolygon + +import logging +from copy import deepcopy +import numpy as np +from collections import Iterable + +import gettext +import FlatCAMTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + +log = logging.getLogger('base') + + +class ToolCopperFill(FlatCAMTool): + + toolName = _("Copper Fill Tool") + + def __init__(self, app): + FlatCAMTool.__init__(self, app) + + self.app = app + self.canvas = self.app.plotcanvas + + self.decimals = 4 + self.units = '' + + # ## Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + self.layout.addWidget(QtWidgets.QLabel('')) + + # ## Grid Layout + i_grid_lay = QtWidgets.QGridLayout() + self.layout.addLayout(i_grid_lay) + i_grid_lay.setColumnStretch(0, 0) + i_grid_lay.setColumnStretch(1, 1) + + self.grb_object_combo = QtWidgets.QComboBox() + 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.setCurrentIndex(1) + + self.grbobj_label = QtWidgets.QLabel("%s:" % _("GERBER")) + self.grbobj_label.setToolTip( + _("Gerber Object to which will be added a copper fill.") + ) + + i_grid_lay.addWidget(self.grbobj_label, 0, 0) + i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2) + i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0) + + # ## Grid Layout + grid_lay = QtWidgets.QGridLayout() + self.layout.addLayout(grid_lay) + grid_lay.setColumnStretch(0, 0) + grid_lay.setColumnStretch(1, 1) + + self.copper_fill_label = QtWidgets.QLabel('%s' % _('Parameters')) + self.copper_fill_label.setToolTip( + _("Parameters used for this tool.") + ) + grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2) + + # CLEARANCE # + self.clearance_label = QtWidgets.QLabel('%s:' % _("Clearance")) + self.clearance_label.setToolTip( + _("This set the distance between the copper fill components\n" + "(the polygon fill may be split in multiple polygons)\n" + "and the copper traces in the Gerber file.") + ) + self.clearance_entry = FCDoubleSpinner() + self.clearance_entry.setMinimum(0.00001) + self.clearance_entry.set_precision(self.decimals) + self.clearance_entry.setSingleStep(0.1) + + grid_lay.addWidget(self.clearance_label, 1, 0) + grid_lay.addWidget(self.clearance_entry, 1, 1) + + # MARGIN # + self.margin_label = QtWidgets.QLabel('%s:' % _("Margin")) + self.margin_label.setToolTip( + _("Bounding box margin.") + ) + self.margin_entry = FCDoubleSpinner() + self.margin_entry.setMinimum(0.0) + self.margin_entry.set_precision(self.decimals) + self.margin_entry.setSingleStep(0.1) + + grid_lay.addWidget(self.margin_label, 2, 0) + grid_lay.addWidget(self.margin_entry, 2, 1) + + # Reference # + self.reference_radio = RadioSet([ + {'label': _('Itself'), 'value': 'itself'}, + {"label": _("Area Selection"), "value": "area"}, + {'label': _("Reference Object"), 'value': 'box'} + ], orientation='vertical', stretch=False) + self.reference_label = QtWidgets.QLabel(_("Reference:")) + self.reference_label.setToolTip( + _("- 'Itself' - the copper fill extent is based on the object that is copper cleared.\n " + "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n" + "- 'Reference Object' - will do copper filling within the area specified by another object.") + ) + grid_lay.addWidget(self.reference_label, 3, 0) + grid_lay.addWidget(self.reference_radio, 3, 1) + + self.box_combo_type_label = QtWidgets.QLabel('%s:' % _("Ref. Type")) + self.box_combo_type_label.setToolTip( + _("The type of FlatCAM object to be used as copper filling reference.\n" + "It can be Gerber, Excellon or Geometry.") + ) + self.box_combo_type = QtWidgets.QComboBox() + self.box_combo_type.addItem(_("Gerber Reference Box Object")) + self.box_combo_type.addItem(_("Excellon Reference Box Object")) + self.box_combo_type.addItem(_("Geometry Reference Box Object")) + + grid_lay.addWidget(self.box_combo_type_label, 4, 0) + grid_lay.addWidget(self.box_combo_type, 4, 1) + + self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object")) + self.box_combo_label.setToolTip( + _("The FlatCAM object to be used as non copper clearing reference.") + ) + self.box_combo = QtWidgets.QComboBox() + self.box_combo.setModel(self.app.collection) + self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.box_combo.setCurrentIndex(1) + + grid_lay.addWidget(self.box_combo_label, 5, 0) + grid_lay.addWidget(self.box_combo, 5, 1) + + self.box_combo.hide() + self.box_combo_label.hide() + self.box_combo_type.hide() + self.box_combo_type_label.hide() + + # ## Insert Copper Fill + self.fill_button = QtWidgets.QPushButton(_("Insert Copper Fill")) + self.fill_button.setToolTip( + _("Will add a polygon (may be split in multiple parts)\n" + "that will surround the actual Gerber traces at a certain distance.") + ) + self.layout.addWidget(self.fill_button) + + self.layout.addStretch() + + # Objects involved in Copper filling + self.grb_object = None + self.ref_obj = None + self.sel_rect = list() + + # Events ID + self.mr = None + self.mm = None + + # Mouse cursor positions + self.mouse_is_dragging = False + self.cursor_pos = (0, 0) + self.first_click = False + + # Tool properties + self.clearance_val = None + self.margin_val = None + self.geo_steps_per_circle = 128 + + # SIGNALS + self.fill_button.clicked.connect(self.execute) + self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type) + self.reference_radio.group_toggle_fn = self.on_toggle_reference + + def run(self, toggle=True): + self.app.report_usage("ToolCopperFill()") + + 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]) + + FlatCAMTool.run(self) + + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Copper Fill Tool")) + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+F', **kwargs) + + def set_tool_ui(self): + self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value() + # self.clearance_entry.set_value(float(self.app.defaults["tools_copperfill_clearance"])) + # self.margin_entry.set_value(float(self.app.defaults["tools_copperfill_margin"])) + # self.reference_radio.set_value(self.app.defaults["tools_copperfill_reference"]) + # self.geo_steps_per_circle = int(self.app.defaults["tools_copperfill_circle_steps"]) + + self.clearance_entry.set_value(0.5) + self.margin_entry.set_value(1.0) + self.reference_radio.set_value('itself') + + def on_combo_box_type(self): + obj_type = self.box_combo_type.currentIndex() + self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) + self.box_combo.setCurrentIndex(0) + + def on_toggle_reference(self): + if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area": + self.box_combo.hide() + self.box_combo_label.hide() + self.box_combo_type.hide() + self.box_combo_type_label.hide() + else: + self.box_combo.show() + self.box_combo_label.show() + self.box_combo_type.show() + self.box_combo_type_label.show() + + def execute(self): + self.clearance_val = self.clearance_entry.get_value() + self.margin_val = self.margin_entry.get_value() + reference_method = self.reference_radio.get_value() + + # get the Gerber object on which the Copper fill will be inserted + selection_index = self.grb_object_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex()) + + try: + self.grb_object = model_index.internalPointer().obj + except Exception as e: + log.debug("ToolCopperFill.execute() --> %s" % str(e)) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) + return 'fail' + + if reference_method == 'itself': + bound_obj_name = self.grb_object_combo.currentText() + + # Get reference object. + try: + self.ref_obj = self.app.collection.get_by_name(bound_obj_name) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e))) + return "Could not retrieve object: %s" % self.obj_name + + self.on_copper_fill( + fill_obj=self.grb_object, + c_val=self.clearance_val, + margin=self.margin_val + ) + + elif reference_method == 'area': + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area.")) + + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) + else: + self.app.plotcanvas.graph_event_disconnect(self.app.mp) + self.app.plotcanvas.graph_event_disconnect(self.app.mm) + self.app.plotcanvas.graph_event_disconnect(self.app.mr) + + self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) + self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) + + elif reference_method == 'box': + bound_obj_name = self.box_combo.currentText() + + # Get reference object. + try: + self.ref_obj = self.app.collection.get_by_name(bound_obj_name) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name)) + return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e)) + + self.on_copper_fill( + fill_obj=self.grb_object, + ref_obj=self.ref_obj, + c_val=self.clearance_val, + margin=self.margin_val + ) + + # To be called after clicking on the plot. + + def on_mouse_release(self, event): + if self.app.is_legacy is False: + event_pos = event.pos + event_is_dragging = event.is_dragging + right_button = 2 + else: + event_pos = (event.xdata, event.ydata) + event_is_dragging = self.app.plotcanvas.is_dragging + right_button = 3 + + event_pos = self.app.plotcanvas.translate_coords(event_pos) + + # do clear area only for left mouse clicks + if event.button == 1: + if self.first_click is False: + self.first_click = True + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area.")) + + self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos) + if self.app.grid_status() == True: + self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) + else: + self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish.")) + + if self.app.grid_status() == True: + curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) + else: + curr_pos = (event_pos[0], event_pos[1]) + + x0, y0 = self.cursor_pos[0], self.cursor_pos[1] + x1, y1 = curr_pos[0], curr_pos[1] + pt1 = (x0, y0) + pt2 = (x1, y0) + pt3 = (x1, y1) + pt4 = (x0, y1) + + self.sel_rect.append(Polygon([pt1, pt2, pt3, pt4])) + self.first_click = False + return + + elif event.button == right_button and self.mouse_is_dragging == False: + self.app.delete_selection_shape() + + self.first_click = False + + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release) + self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) + else: + self.app.plotcanvas.graph_event_disconnect(self.mr) + self.app.plotcanvas.graph_event_disconnect(self.mm) + + self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', + self.app.on_mouse_click_over_plot) + self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', + self.app.on_mouse_move_over_plot) + self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', + self.app.on_mouse_click_release_over_plot) + + if len(self.sel_rect) == 0: + return + + self.sel_rect = cascaded_union(self.sel_rect) + + if not isinstance(self.sel_rect, Iterable): + self.sel_rect = [self.sel_rect] + + self.on_copper_fill( + fill_obj=self.grb_object, + ref_obj=self.sel_rect, + c_val=self.clearance_val, + margin=self.margin_val + ) + + # called on mouse move + def on_mouse_move(self, event): + if self.app.is_legacy is False: + event_pos = event.pos + event_is_dragging = event.is_dragging + right_button = 2 + else: + event_pos = (event.xdata, event.ydata) + event_is_dragging = self.app.plotcanvas.is_dragging + right_button = 3 + + curr_pos = self.app.plotcanvas.translate_coords(event_pos) + + # detect mouse dragging motion + if event_is_dragging is True: + self.mouse_is_dragging = True + else: + self.mouse_is_dragging = False + + # update the cursor position + if self.app.grid_status() == True: + # Update cursor + curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1]) + + self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]), + symbol='++', edge_color=self.app.cursor_color_3D, + size=self.app.defaults["global_cursor_size"]) + + # update the positions on status bar + self.app.ui.position_label.setText("    X: %.4f   " + "Y: %.4f" % (curr_pos[0], curr_pos[1])) + if self.cursor_pos is None: + self.cursor_pos = (0, 0) + + dx = curr_pos[0] - float(self.cursor_pos[0]) + dy = curr_pos[1] - float(self.cursor_pos[1]) + self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " + "%.4f    " % (dx, dy)) + + # draw the utility geometry + if self.first_click: + self.app.delete_selection_shape() + self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]), + coords=(curr_pos[0], curr_pos[1])) + + def on_copper_fill(self, fill_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True): + """ + + :param fill_obj: + :param ref_obj: + :param c_val: + :param margin: + :param run_threaded: + :return: + """ + + if run_threaded: + proc = self.app.proc_container.new('%s ...' % _("Copper filling")) + else: + self.app.proc_container.view.set_busy('%s ...' % _("Copper filling")) + QtWidgets.QApplication.processEvents() + + # ##################################################################### + # ####### Read the parameters ######################################### + # ##################################################################### + + log.debug("Copper Filling Tool started. Reading parameters.") + self.app.inform.emit(_("Copper Filling Tool started. Reading parameters.")) + + units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value() + ref_selected = self.reference_radio.get_value() + if c_val is None: + c_val = float(self.app.defaults["tools_copperfill_clearance"]) + if margin is None: + margin = float(self.app.defaults["tools_copperfill_margin"]) + + # make sure that the source object solid geometry is an Iterable + if not isinstance(self.grb_object.solid_geometry, Iterable): + self.grb_object.solid_geometry = [self.grb_object.solid_geometry] + + # ######################################################################################### + # Prepare isolation polygon. This will create the clearance over the Gerber features ###### + # ######################################################################################### + log.debug("Copper Filling Tool. Preparing isolation polygons.") + self.app.inform.emit(_("Copper Filling Tool. Preparing isolation polygons.")) + + # variables to display the percentage of work done + geo_len = 0 + try: + for pol in self.grb_object.solid_geometry: + geo_len += 1 + except TypeError: + geo_len = 1 + + old_disp_number = 0 + pol_nr = 0 + + clearance_geometry = [] + try: + for pol in self.grb_object.solid_geometry: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + + clearance_geometry.append( + pol.buffer(c_val, int(int(self.geo_steps_per_circle) / 4)) + ) + + pol_nr += 1 + disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) + + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %s ... %d%%' % + (_("Buffering"), int(disp_number))) + old_disp_number = disp_number + except TypeError: + # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a + # MultiPolygon (not an iterable) + clearance_geometry.append( + self.grb_object.solid_geometry.buffer(c_val, int(int(self.geo_steps_per_circle) / 4)) + ) + + self.app.proc_container.update_view_text(' %s' % _("Buffering")) + clearance_geometry = unary_union(clearance_geometry) + + # ######################################################################################### + # Prepare the area to fill with copper. ################################################### + # ######################################################################################### + log.debug("Copper Filling Tool. Preparing areas to fill with copper.") + self.app.inform.emit(_("Copper Filling Tool. Preparing areas to fill with copper.")) + + try: + if ref_obj is None or ref_obj == 'itself': + working_obj = fill_obj + else: + working_obj = ref_obj + except Exception as e: + log.debug("ToolCopperFIll.on_copper_fill() --> %s" % str(e)) + return 'fail' + + bounding_box = None + if ref_selected == 'itself': + geo_n = working_obj.solid_geometry + + try: + if isinstance(geo_n, MultiPolygon): + env_obj = geo_n.convex_hull + elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \ + (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon): + env_obj = cascaded_union(geo_n) + else: + env_obj = cascaded_union(geo_n) + env_obj = env_obj.convex_hull + + bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) + except Exception as e: + log.debug("ToolCopperFIll.on_copper_fill() 'itself' --> %s" % str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available.")) + return 'fail' + + elif ref_selected == 'area': + geo_n = cascaded_union(working_obj) + try: + __ = iter(geo_n) + except Exception as e: + log.debug("ToolCopperFIll.on_copper_fill() 'area' --> %s" % str(e)) + geo_n = [geo_n] + + geo_buff_list = [] + for poly in geo_n: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + geo_buff_list.append(poly.buffer(distance=0.0, join_style=base.JOIN_STYLE.mitre)) + + bounding_box = cascaded_union(geo_buff_list) + + elif ref_selected == 'box': + geo_n = working_obj.solid_geometry + + if isinstance(working_obj, FlatCAMGeometry): + try: + __ = iter(geo_n) + except Exception as e: + log.debug("ToolCopperFIll.on_copper_fill() 'box' --> %s" % str(e)) + geo_n = [geo_n] + + geo_buff_list = [] + for poly in geo_n: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)) + + bounding_box = cascaded_union(geo_buff_list) + elif isinstance(working_obj, FlatCAMGerber): + geo_n = cascaded_union(geo_n).convex_hull + bounding_box = cascaded_union(self.ncc_obj.solid_geometry).convex_hull.intersection(geo_n) + bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre) + else: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported.")) + return 'fail' + + log.debug("Copper Filling Tool. Finished creating areas to fill with copper.") + + self.app.inform.emit(_("Copper Filling Tool. Appending new geometry and buffering.")) + new_solid_geometry = bounding_box.difference(clearance_geometry) + + geo_list = self.grb_object.solid_geometry + if isinstance(self.grb_object.solid_geometry, MultiPolygon): + geo_list = list(self.grb_object.solid_geometry.geoms) + + if '0' not in self.grb_object.apertures: + self.grb_object.apertures['0'] = dict() + self.grb_object.apertures['0']['geometry'] = list() + self.grb_object.apertures['0']['type'] = 'REG' + self.grb_object.apertures['0']['size'] = 0.0 + + try: + for poly in new_solid_geometry: + # append to the new solid geometry + geo_list.append(poly) + + # append into the '0' aperture + geo_elem = dict() + geo_elem['solid'] = poly + geo_elem['follow'] = poly.exterior + self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem)) + except TypeError: + # append to the new solid geometry + geo_list.append(new_solid_geometry) + + # append into the '0' aperture + geo_elem = dict() + geo_elem['solid'] = new_solid_geometry + geo_elem['follow'] = new_solid_geometry.exterior + self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem)) + + self.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001) + + # update the source file with the new geometry: + self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None, + local_use=self.grb_object, use_thread=False) + + self.on_exit() + self.app.inform.emit('[success] %s' % _("Copper Fill Tool done.")) + + def replot(self, obj): + def worker_task(): + with self.app.proc_container.new('%s...' % _("Plotting")): + obj.plot() + + self.app.worker_task.emit({'fcn': worker_task, 'params': []}) + + def on_exit(self): + # plot the object + self.replot(obj=self.grb_object) + + # update the bounding box values + try: + a, b, c, d = self.grb_object.bounds() + self.grb_object.options['xmin'] = a + self.grb_object.options['ymin'] = b + self.grb_object.options['xmax'] = c + self.grb_object.options['ymax'] = d + except Exception as e: + log.debug("ToolCopperFill.on_exit() bounds error --> %s" % str(e)) + + # reset the variables + self.grb_object = None + self.ref_obj = None + self.sel_rect = list() + + # Events ID + self.mr = None + self.mm = None + + # Mouse cursor positions + self.mouse_is_dragging = False + self.cursor_pos = (0, 0) + self.first_click = False diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index 341f58e5..deb35b20 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -429,12 +429,9 @@ class NonCopperClear(FlatCAMTool, Gerber): ], orientation='vertical', stretch=False) self.reference_label = QtWidgets.QLabel(_("Reference:")) self.reference_label.setToolTip( - _("- 'Itself' - the non copper clearing extent\n" - "is based on the object that is copper cleared.\n " + _("- 'Itself' - the non copper clearing extent is based on the object that is copper cleared.\n " "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n" - "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n" - "- 'Reference Object' - will do non copper clearing within the area\n" - "specified by another object.") + "- 'Reference Object' - will do non copper clearing within the area specified by another object.") ) grid3.addWidget(self.reference_label, 10, 0) grid3.addWidget(self.reference_radio, 10, 1) @@ -1174,8 +1171,8 @@ class NonCopperClear(FlatCAMTool, Gerber): try: self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name) except Exception as e: - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name)) - return "Could not retrieve object: %s" % self.obj_name + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.bound_obj_name)) + return "Could not retrieve object: %s" % self.bound_obj_name self.clear_copper(ncc_obj=self.ncc_obj, ncctooldia=self.ncc_dia_list, @@ -1244,7 +1241,6 @@ class NonCopperClear(FlatCAMTool, Gerber): self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) else: self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish.")) - self.app.delete_selection_shape() if self.app.grid_status() == True: curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) @@ -1263,6 +1259,8 @@ class NonCopperClear(FlatCAMTool, Gerber): return elif event.button == right_button and self.mouse_is_dragging == False: + self.app.delete_selection_shape() + self.first_click = False if self.app.is_legacy is False: diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 07247783..95b8c9e3 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -1122,7 +1122,6 @@ class ToolPaint(FlatCAMTool, Gerber): self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1]) else: self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish.")) - self.app.delete_selection_shape() curr_pos = self.app.plotcanvas.translate_coords(event_pos) if self.app.grid_status() == True: @@ -1139,6 +1138,8 @@ class ToolPaint(FlatCAMTool, Gerber): return elif event.button == right_button and self.mouse_is_dragging is False: + self.app.delete_selection_shape() + self.first_click = False if self.app.is_legacy is False: diff --git a/flatcamTools/ToolQRCode.py b/flatcamTools/ToolQRCode.py index 116f6a78..bfeb7cdf 100644 --- a/flatcamTools/ToolQRCode.py +++ b/flatcamTools/ToolQRCode.py @@ -9,7 +9,7 @@ from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt from FlatCAMTool import FlatCAMTool -from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox, OptionalInputSection +from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox from flatcamParsers.ParseSVG import * from shapely.geometry.base import * @@ -91,8 +91,7 @@ class QRCode(FlatCAMTool): self.qrcode_label = QtWidgets.QLabel('%s' % _('QRCode Parameters')) self.qrcode_label.setToolTip( - _("Contain the expected calibration points and the\n" - "ones measured.") + _("The parameters used to shape the QRCode.") ) grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2) @@ -445,9 +444,9 @@ class QRCode(FlatCAMTool): def make(self, pos): self.on_exit() - # add the svg geometry to the selected Gerber object solid_geometry and in obj.apertures, apid = 0 + # make sure that the source object solid geometry is an Iterable if not isinstance(self.grb_object.solid_geometry, Iterable): - self.grb_object.solid_geometry = list(self.grb_object.solid_geometry) + self.grb_object.solid_geometry = [self.grb_object.solid_geometry] # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered geo_list = self.grb_object.solid_geometry @@ -553,6 +552,7 @@ class QRCode(FlatCAMTool): local_use=self.grb_object, use_thread=False) self.replot(obj=self.grb_object) + self.app.inform.emit('[success] %s' % _("QRCode Tool done.")) def draw_utility_geo(self, pos): diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index 87b63d10..3b4ceed2 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -29,6 +29,8 @@ from flatcamTools.ToolProperties import Properties from flatcamTools.ToolQRCode import QRCode from flatcamTools.ToolRulesCheck import RulesCheck +from flatcamTools.ToolCopperFill import ToolCopperFill + from flatcamTools.ToolShell import FCShell from flatcamTools.ToolSolderPaste import SolderPaste from flatcamTools.ToolSub import ToolSub diff --git a/share/copperfill16.png b/share/copperfill16.png new file mode 100644 index 0000000000000000000000000000000000000000..422970f19ab4881c7ba1799759339e53640e5897 GIT binary patch literal 569 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+uenMVO6iP5s=4O z;1OBOz`%C|gc+x5^GO2**-JcqUD+RVN%G06$waYVW?*1s_jGX#(Kz3F(O!dvLFD*H z|MI+v_UW$Mx?M!fZ)~`8a(*KZTX$08BplUJ$UwU5tp5hXg zs+e?eRp4{Iy*pT+$DO@-%38*ArepKn_ zJ+g3Ke)|p|za=JJRv&UAog%_yuG~7u$XUA}ILh|WrdzCB4o4g>hTc+9J(?qYV5_yT zt3zcR+kq{LkL_O@CNt^n`FVo*{jaUJ#KjG>9kzEQ$NK$OyB@tIHHzcxwX@uRzh~Dl zZaHSZ=U1oaePAf5mbgZgq$HN4S|t~y0x1R~14Bz)LjzqS(-1=wD68;Q8eV{r(~v8;?|(`sM#B+K@wy`aDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ QQb0uvp00i_>zopr0O!)nmH+?% literal 0 HcmV?d00001 diff --git a/share/copperfill32.png b/share/copperfill32.png new file mode 100644 index 0000000000000000000000000000000000000000..b0f5704b8307552e957bd0c2d9de9a671f892174 GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-GzZ+Rj;xUkjGiz z5n0T@z;^_M8K-LVNdpDhOFVsD*&lOB^2rK&y*&H@sHw-(#WBR=_}Qz`9alp|j(SKt$%h>emOM1omR)v0hUd>+{ z)g02(@wG2HX@4*4y5zqLF0%f0?XX;yH1Yjs?v5{~o*dhHYqFH#^@i1HB8;y}xUbz2 zRy+ACWN|$24V{H<{R*38&wIYscH9$Tk+zy;wZZBBru%cm-(1klmYL?cJE%nN{2kVi zCaF7arw-rW;;c=~N&_4~|sw%wCGex{c<{$<9ze4U2wFz**#J2!<{ueqPSqVVqQ zb1Pk~_$<7=HmrYeEK*%Lb(Xu!&ib#h2haS7np}4B)Qb%#gQIF*