|
@@ -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("<b>%s:</b>" % _("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('<b>%s</b>' % _('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(" <b>X</b>: %.4f "
|
|
|
|
|
+ "<b>Y</b>: %.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("<b>Dx</b>: %.4f <b>Dy</b>: "
|
|
|
|
|
+ "%.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
|