# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # File Author: Marius Adrian Stanciu (c) # # Date: 1/24/2020 # # MIT Licence # # ########################################################## from PyQt5 import QtGui, QtCore, QtWidgets from FlatCAMTool import FlatCAMTool from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \ OptionalHideInputSection, OptionalInputSection, FCComboBox from copy import deepcopy import logging from shapely.geometry import Polygon, MultiPolygon, Point import gettext import FlatCAMTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class ToolPunchGerber(FlatCAMTool): toolName = _("Punch Gerber") def __init__(self, app): FlatCAMTool.__init__(self, app) self.decimals = self.app.decimals # Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; } """) self.layout.addWidget(title_label) # Punch Drill holes self.layout.addWidget(QtWidgets.QLabel("")) # ## Grid Layout grid_lay = QtWidgets.QGridLayout() self.layout.addLayout(grid_lay) grid_lay.setColumnStretch(0, 1) grid_lay.setColumnStretch(1, 0) # ## Gerber Object self.gerber_object_combo = QtWidgets.QComboBox() self.gerber_object_combo.setModel(self.app.collection) self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.gerber_object_combo.setCurrentIndex(1) self.grb_label = QtWidgets.QLabel("%s:" % _("GERBER")) self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes")) grid_lay.addWidget(self.grb_label, 0, 0, 1, 2) grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid_lay.addWidget(separator_line, 2, 0, 1, 2) self.padt_label = QtWidgets.QLabel("%s" % _("Processed Pads Type")) self.padt_label.setToolTip( _("The type of pads shape to be processed.\n" "If the PCB has many SMD pads with rectangular pads,\n" "disable the Rectangular aperture.") ) grid_lay.addWidget(self.padt_label, 3, 0, 1, 2) # Select all self.select_all_cb = FCCheckBox('%s' % _("ALL")) grid_lay.addWidget(self.select_all_cb) # Circular Aperture Selection self.circular_cb = FCCheckBox('%s' % _("Circular")) self.circular_cb.setToolTip( _("Create drills from circular pads.") ) grid_lay.addWidget(self.circular_cb, 5, 0, 1, 2) # Oblong Aperture Selection self.oblong_cb = FCCheckBox('%s' % _("Oblong")) self.oblong_cb.setToolTip( _("Create drills from oblong pads.") ) grid_lay.addWidget(self.oblong_cb, 6, 0, 1, 2) # Square Aperture Selection self.square_cb = FCCheckBox('%s' % _("Square")) self.square_cb.setToolTip( _("Create drills from square pads.") ) grid_lay.addWidget(self.square_cb, 7, 0, 1, 2) # Rectangular Aperture Selection self.rectangular_cb = FCCheckBox('%s' % _("Rectangular")) self.rectangular_cb.setToolTip( _("Create drills from rectangular pads.") ) grid_lay.addWidget(self.rectangular_cb, 8, 0, 1, 2) # Others type of Apertures Selection self.other_cb = FCCheckBox('%s' % _("Others")) self.other_cb.setToolTip( _("Create drills from other types of pad shape.") ) grid_lay.addWidget(self.other_cb, 9, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid_lay.addWidget(separator_line, 10, 0, 1, 2) # Grid Layout grid0 = QtWidgets.QGridLayout() self.layout.addLayout(grid0) grid0.setColumnStretch(0, 0) grid0.setColumnStretch(1, 1) self.method_label = QtWidgets.QLabel('%s:' % _("Method")) self.method_label.setToolTip( _("The punch hole source can be:\n" "- Excellon Object-> the Excellon object drills center will serve as reference.\n" "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n" "- Fixed Annular Ring -> will try to keep a set annular ring.\n" "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.\n") ) self.method_punch = RadioSet( [ {'label': _('Excellon'), 'value': 'exc'}, {'label': _("Fixed Diameter"), 'value': 'fixed'}, {'label': _("Fixed Annular Ring"), 'value': 'ring'}, {'label': _("Proportional"), 'value': 'prop'} ], orientation='vertical', stretch=False) grid0.addWidget(self.method_label, 0, 0, 1, 2) grid0.addWidget(self.method_punch, 1, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 2, 0, 1, 2) self.exc_label = QtWidgets.QLabel('%s' % _("Excellon")) self.exc_label.setToolTip( _("Remove the geometry of Excellon from the Gerber to create the holes in pads.") ) self.exc_combo = QtWidgets.QComboBox() self.exc_combo.setModel(self.app.collection) self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) self.exc_combo.setCurrentIndex(1) grid0.addWidget(self.exc_label, 3, 0, 1, 2) grid0.addWidget(self.exc_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) # Fixed Dia self.fixed_label = QtWidgets.QLabel('%s' % _("Fixed Diameter")) grid0.addWidget(self.fixed_label, 6, 0, 1, 2) # Diameter value self.dia_entry = FCDoubleSpinner() self.dia_entry.set_precision(self.decimals) self.dia_entry.set_range(0.0000, 9999.9999) self.dia_label = QtWidgets.QLabel('%s:' % _("Value")) self.dia_label.setToolTip( _("Fixed hole diameter.") ) grid0.addWidget(self.dia_label, 8, 0) grid0.addWidget(self.dia_entry, 8, 1) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 9, 0, 1, 2) self.ring_frame = QtWidgets.QFrame() self.ring_frame.setContentsMargins(0, 0, 0, 0) grid0.addWidget(self.ring_frame, 10, 0, 1, 2) self.ring_box = QtWidgets.QVBoxLayout() self.ring_box.setContentsMargins(0, 0, 0, 0) self.ring_frame.setLayout(self.ring_box) # Annular Ring value self.ring_label = QtWidgets.QLabel('%s' % _("Fixed Annular Ring")) self.ring_label.setToolTip( _("The size of annular ring.\n" "The copper sliver between the drill hole exterior\n" "and the margin of the copper pad.") ) self.ring_box.addWidget(self.ring_label) # ## Grid Layout self.grid1 = QtWidgets.QGridLayout() self.grid1.setColumnStretch(0, 0) self.grid1.setColumnStretch(1, 1) self.ring_box.addLayout(self.grid1) # Circular Annular Ring Value self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular")) self.circular_ring_label.setToolTip( _("The size of annular ring for circular pads.") ) self.circular_ring_entry = FCDoubleSpinner() self.circular_ring_entry.set_precision(self.decimals) self.circular_ring_entry.set_range(0.0000, 9999.9999) self.grid1.addWidget(self.circular_ring_label, 3, 0) self.grid1.addWidget(self.circular_ring_entry, 3, 1) # Oblong Annular Ring Value self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong")) self.oblong_ring_label.setToolTip( _("The size of annular ring for oblong pads.") ) self.oblong_ring_entry = FCDoubleSpinner() self.oblong_ring_entry.set_precision(self.decimals) self.oblong_ring_entry.set_range(0.0000, 9999.9999) self.grid1.addWidget(self.oblong_ring_label, 4, 0) self.grid1.addWidget(self.oblong_ring_entry, 4, 1) # Square Annular Ring Value self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square")) self.square_ring_label.setToolTip( _("The size of annular ring for square pads.") ) self.square_ring_entry = FCDoubleSpinner() self.square_ring_entry.set_precision(self.decimals) self.square_ring_entry.set_range(0.0000, 9999.9999) self.grid1.addWidget(self.square_ring_label, 5, 0) self.grid1.addWidget(self.square_ring_entry, 5, 1) # Rectangular Annular Ring Value self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular")) self.rectangular_ring_label.setToolTip( _("The size of annular ring for rectangular pads.") ) self.rectangular_ring_entry = FCDoubleSpinner() self.rectangular_ring_entry.set_precision(self.decimals) self.rectangular_ring_entry.set_range(0.0000, 9999.9999) self.grid1.addWidget(self.rectangular_ring_label, 6, 0) self.grid1.addWidget(self.rectangular_ring_entry, 6, 1) # Others Annular Ring Value self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others")) self.other_ring_label.setToolTip( _("The size of annular ring for other pads.") ) self.other_ring_entry = FCDoubleSpinner() self.other_ring_entry.set_precision(self.decimals) self.other_ring_entry.set_range(0.0000, 9999.9999) self.grid1.addWidget(self.other_ring_label, 7, 0) self.grid1.addWidget(self.other_ring_entry, 7, 1) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 11, 0, 1, 2) # Proportional value self.prop_label = QtWidgets.QLabel('%s' % _("Proportional Diameter")) grid0.addWidget(self.prop_label, 12, 0, 1, 2) # Diameter value self.factor_entry = FCDoubleSpinner(suffix='%') self.factor_entry.set_precision(self.decimals) self.factor_entry.set_range(0.0000, 100.0000) self.factor_entry.setSingleStep(0.1) self.factor_label = QtWidgets.QLabel('%s:' % _("Value")) self.factor_label.setToolTip( _("Proportional Diameter.\n" "The drill diameter will be a fraction of the pad size.") ) grid0.addWidget(self.factor_label, 13, 0) grid0.addWidget(self.factor_entry, 13, 1) separator_line3 = QtWidgets.QFrame() separator_line3.setFrameShape(QtWidgets.QFrame.HLine) separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line3, 14, 0, 1, 2) # Buttons self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber")) self.punch_object_button.setToolTip( _("Create a Gerber object from the selected object, within\n" "the specified box.") ) self.punch_object_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.layout.addWidget(self.punch_object_button) self.layout.addStretch() # ## Reset Tool self.reset_button = QtWidgets.QPushButton(_("Reset Tool")) self.reset_button.setToolTip( _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" QPushButton { font-weight: bold; } """) self.layout.addWidget(self.reset_button) self.units = self.app.defaults['units'] # self.cb_items = [ # self.grid1.itemAt(w).widget() for w in range(self.grid1.count()) # if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox) # ] self.circular_ring_entry.setEnabled(False) self.oblong_ring_entry.setEnabled(False) self.square_ring_entry.setEnabled(False) self.rectangular_ring_entry.setEnabled(False) self.other_ring_entry.setEnabled(False) self.dia_entry.setDisabled(True) self.dia_label.setDisabled(True) self.factor_label.setDisabled(True) self.factor_entry.setDisabled(True) # ## Signals self.method_punch.activated_custom.connect(self.on_method) self.reset_button.clicked.connect(self.set_tool_ui) self.punch_object_button.clicked.connect(self.on_generate_object) self.circular_cb.stateChanged.connect( lambda state: self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True) ) self.oblong_cb.stateChanged.connect( lambda state: self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True) ) self.square_cb.stateChanged.connect( lambda state: self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True) ) self.rectangular_cb.stateChanged.connect( lambda state: self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True) ) self.other_cb.stateChanged.connect( lambda state: self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True) ) def run(self, toggle=True): self.app.report_usage("ToolPunchGerber()") 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, _("Punch Tool")) def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+H', **kwargs) def set_tool_ui(self): self.reset_fields() self.ui_connect() self.method_punch.set_value('exc') self.select_all_cb.set_value(True) def on_select_all(self, state): self.ui_disconnect() if state: self.circular_cb.setChecked(True) self.oblong_cb.setChecked(True) self.square_cb.setChecked(True) self.rectangular_cb.setChecked(True) self.other_cb.setChecked(True) else: self.circular_cb.setChecked(False) self.oblong_cb.setChecked(False) self.square_cb.setChecked(False) self.rectangular_cb.setChecked(False) self.other_cb.setChecked(False) self.ui_connect() def on_method(self, val): self.exc_label.setEnabled(False) self.exc_combo.setEnabled(False) self.fixed_label.setEnabled(False) self.dia_label.setEnabled(False) self.dia_entry.setEnabled(False) self.ring_frame.setEnabled(False) self.prop_label.setEnabled(False) self.factor_label.setEnabled(False) self.factor_entry.setEnabled(False) if val == 'exc': self.exc_label.setEnabled(True) self.exc_combo.setEnabled(True) elif val == 'fixed': self.fixed_label.setEnabled(True) self.dia_label.setEnabled(True) self.dia_entry.setEnabled(True) elif val == 'ring': self.ring_frame.setEnabled(True) elif val == 'prop': self.prop_label.setEnabled(True) self.factor_label.setEnabled(True) self.factor_entry.setEnabled(True) def ui_connect(self): self.select_all_cb.stateChanged.connect(self.on_select_all) def ui_disconnect(self): try: self.select_all_cb.stateChanged.disconnect() except (AttributeError, TypeError): pass def on_generate_object(self): # get the Gerber file who is the source of the punched Gerber selection_index = self.gerber_object_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex()) try: grb_obj = model_index.internalPointer().obj except Exception: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return name = grb_obj.options['name'].rpartition('.')[0] outname = name + "_punched" punch_method = self.method_punch.get_value() if punch_method == 'exc': # get the Excellon file whose geometry will create the punch holes selection_index = self.exc_combo.currentIndex() model_index = self.app.collection.index(selection_index, 0, self.exc_combo.rootModelIndex()) try: exc_obj = model_index.internalPointer().obj except Exception: self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) return # this is the punching geometry exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry) if isinstance(grb_obj.solid_geometry, list): grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry) else: grb_solid_geometry = grb_obj.solid_geometry # create the punched Gerber solid_geometry punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry) new_apertures = dict() new_apertures = deepcopy(grb_obj.apertures) holes_apertures = dict() for apid, val in new_apertures.items(): for elem in val['geometry']: # make it work only for Gerber Flashes who are Points in 'follow' if 'solid' in elem and isinstance(elem['follow'], Point): for drill in exc_obj.drills: clear_apid = exc_obj.tools[drill['tool']]['C'] exc_poly = drill['point'].buffer(clear_apid / 2.0) if exc_poly.within(elem['solid']): if clear_apid not in holes_apertures or holes_apertures[clear_apid]['type'] != 'C': holes_apertures[clear_apid] = dict() holes_apertures[clear_apid]['type'] = 'C' holes_apertures[clear_apid]['size'] = clear_apid holes_apertures[clear_apid]['geometry'] = list() geo_elem = dict() geo_elem['clear'] = exc_poly geo_elem['follow'] = exc_poly.centroid holes_apertures[clear_apid]['geometry'].append(deepcopy(geo_elem)) elem['clear'] = exc_poly.centroid for apid, val in new_apertures.items(): for clear_apid, clear_val in holes_apertures.items(): if round(clear_apid, self.decimals) == round(val['size'], self.decimals): geo_elem = dict() val['geometry'].append(geo_elem) def init_func(new_obj, app_obj): new_obj.options.update(grb_obj.options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) new_obj.outline_color = deepcopy(grb_obj.outline_color) new_obj.apertures = deepcopy(new_apertures) new_obj.solid_geometry = deepcopy(punched_solid_geometry) new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, local_use=new_obj, use_thread=False) self.app.new_object('gerber', outname, init_func) elif punch_method == 'fixed': punch_size = float(self.dia_entry.get_value()) punching_geo = list() for apid in grb_obj.apertures: if grb_obj.apertures[apid]['type'] == 'C': if punch_size >= float(grb_obj.apertures[apid]['size']): self.app.inform.emit('[ERROR_NOTCL] %s' % _(" Could not generate punched hole Gerber because the punch hole size" "is bigger than some of the apertures in the Gerber object.")) return 'fail' else: for elem in grb_obj.apertures[apid]['geometry']: if 'follow' in elem: if isinstance(elem['follow'], Point): punching_geo.append(elem['follow'].buffer(punch_size / 2)) else: if punch_size >= float(grb_obj.apertures[apid]['width']) or \ punch_size >= float(grb_obj.apertures[apid]['height']): self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not generate punched hole Gerber because the punch hole size" "is bigger than some of the apertures in the Gerber object.")) return 'fail' else: for elem in grb_obj.apertures[apid]['geometry']: if 'follow' in elem: if isinstance(elem['follow'], Point): punching_geo.append(elem['follow'].buffer(punch_size / 2)) punching_geo = MultiPolygon(punching_geo) if isinstance(grb_obj.solid_geometry, list): temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) else: temp_solid_geometry = grb_obj.solid_geometry punched_solid_geometry = temp_solid_geometry.difference(punching_geo) if punched_solid_geometry == temp_solid_geometry: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Could not generate punched hole Gerber because the newly created object " "geometry is the same as the one in the source object geometry...")) return 'fail' def init_func(new_obj, app_obj): new_obj.solid_geometry = deepcopy(punched_solid_geometry) self.app.new_object('gerber', outname, init_func) elif punch_method == 'ring': pass elif punch_method == 'prop': pass def reset_fields(self): self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) self.ui_disconnect()