diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c61837..5fb126d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG for FlatCAM beta - in Cutout Tool added the UI for a new feature: Cut by Drilling - fixed a bug in Extract Tool, when extracting drills some of the drills were lost; added a new UI control to select/deselect all apertures - updated the Extract Tool - Extract Soldermask functionality, such that the selection of apertures will control the final SolderMask Gerber content +- updated the Extract Tool - new functionality added: Extract Cutout Gerber from a given Gerber object; added parameters in Preferences 9.11.2020 diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index d2a56014..68e6caa8 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -646,6 +646,8 @@ class PreferencesUIManager: "tools_extract_rectangular": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_cb, "tools_extract_others": self.ui.tools2_defaults_form.tools2_edrills_group.other_cb, "tools_extract_sm_clearance": self.ui.tools2_defaults_form.tools2_edrills_group.clearance_entry, + "tools_extract_cut_margin": self.ui.tools2_defaults_form.tools2_edrills_group.margin_cut_entry, + "tools_extract_cut_thickness": self.ui.tools2_defaults_form.tools2_edrills_group.thick_cut_entry, # Punch Gerber Tool "tools_punch_hole_type": self.ui.tools2_defaults_form.tools2_punch_group.hole_size_radio, diff --git a/appGUI/preferences/tools/Tools2ExtractPrefGroupUI.py b/appGUI/preferences/tools/Tools2ExtractPrefGroupUI.py index 8623bdb9..975a458c 100644 --- a/appGUI/preferences/tools/Tools2ExtractPrefGroupUI.py +++ b/appGUI/preferences/tools/Tools2ExtractPrefGroupUI.py @@ -247,5 +247,39 @@ class Tools2EDrillsPrefGroupUI(OptionsGroupUI): grid_lay.addWidget(self.clearance_label, 24, 0) grid_lay.addWidget(self.clearance_entry, 24, 1) + + # EXTRACT CUTOUT + self.extract_cut_label = FCLabel('%s' % _("Extract Cutout")) + self.extract_cut_label.setToolTip( + _("Extract a cutout from a given Gerber file.")) + grid_lay.addWidget(self.extract_cut_label, 26, 0, 1, 2) + + # Margin Cutout + self.margin_cut_label = FCLabel('%s:' % _("Margin")) + self.margin_cut_label.setToolTip( + _("Margin over bounds. A positive value here\n" + "will make the cutout of the PCB further from\n" + "the actual PCB border") + ) + self.margin_cut_entry = FCDoubleSpinner() + self.margin_cut_entry.set_range(-10000.0000, 10000.0000) + self.margin_cut_entry.set_precision(self.decimals) + self.margin_cut_entry.setSingleStep(0.1) + + grid_lay.addWidget(self.margin_cut_label, 28, 0) + grid_lay.addWidget(self.margin_cut_entry, 28, 1) + + # Thickness Cutout + self.thick_cut_label = FCLabel('%s:' % _("Thickness")) + self.thick_cut_label.setToolTip( + _("The thickness of the line that makes the cutout geometry.") + ) + self.thick_cut_entry = FCDoubleSpinner() + self.thick_cut_entry.set_range(0.0000, 10000.0000) + self.thick_cut_entry.set_precision(self.decimals) + self.thick_cut_entry.setSingleStep(0.1) + + grid_lay.addWidget(self.thick_cut_label, 30, 0) + grid_lay.addWidget(self.thick_cut_entry, 30, 1) self.layout.addStretch() diff --git a/appTools/ToolExtract.py b/appTools/ToolExtract.py index 87545b18..7cef4bc7 100644 --- a/appTools/ToolExtract.py +++ b/appTools/ToolExtract.py @@ -10,7 +10,8 @@ from PyQt5 import QtWidgets, QtCore, QtGui from appTool import AppTool from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox, FCLabel -from shapely.geometry import Point +from shapely.geometry import Point, MultiPolygon, Polygon, box +from shapely.ops import unary_union from copy import deepcopy @@ -40,9 +41,6 @@ class ToolExtract(AppTool): # ## Signals self.ui.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle) - self.ui.e_drills_button.clicked.connect(self.on_extract_drills_click) - self.ui.e_sm_button.clicked.connect(self.on_extract_soldermask_click) - self.ui.reset_button.clicked.connect(self.set_tool_ui) self.ui.circular_cb.stateChanged.connect( lambda state: @@ -72,6 +70,11 @@ class ToolExtract(AppTool): self.ui.all_cb.stateChanged.connect(self.on_select_all) + self.ui.e_drills_button.clicked.connect(self.on_extract_drills_click) + self.ui.e_sm_button.clicked.connect(self.on_extract_soldermask_click) + self.ui.e_cut_button.clicked.connect(self.on_extract_cutout_click) + self.ui.reset_button.clicked.connect(self.set_tool_ui) + def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs) @@ -123,8 +126,13 @@ class ToolExtract(AppTool): self.ui.factor_entry.set_value(float(self.app.defaults["tools_extract_hole_prop_factor"])) + # Extract Soldermask self.ui.clearance_entry.set_value(float(self.app.defaults["tools_extract_sm_clearance"])) + # Extract Cutout + self.ui.margin_cut_entry.set_value(float(self.app.defaults["tools_extract_cut_margin"])) + self.ui.thick_cut_entry.set_value(float(self.app.defaults["tools_extract_cut_thickness"])) + def on_select_all(self, state): if state: @@ -435,7 +443,7 @@ class ToolExtract(AppTool): self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) return - outname = '%s_sm' % obj.options['name'].rpartition('.')[0] + outname = '%s_esm' % obj.options['name'].rpartition('.')[0] new_apertures = deepcopy(obj.apertures) new_solid_geometry = [] @@ -511,6 +519,81 @@ class ToolExtract(AppTool): log.error("Error on Extracted Soldermask Gerber object creation: %s" % str(e)) return + def on_extract_cutout_click(self): + margin = self.ui.margin_cut_entry.get_value() + thickness = self.ui.thick_cut_entry.get_value() + + buff_radius = thickness / 2.0 + + selection_index = self.ui.gerber_object_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex()) + + try: + obj = model_index.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) + return + + outname = '%s_ecut' % obj.options['name'].rpartition('.')[0] + + cut_solid_geometry = obj.solid_geometry + if isinstance(obj.solid_geometry, list): + cut_solid_geometry = MultiPolygon(obj.solid_geometry) + + if isinstance(cut_solid_geometry, (MultiPolygon, Polygon)): + x0, y0, x1, y1 = cut_solid_geometry.bounds + object_geo = box(x0, y0, x1, y1) + else: + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("No cutout extracted."))) + return + + try: + geo_buf = object_geo.buffer(margin) + new_geo_follow = geo_buf.exterior + new_geo_solid = new_geo_follow.buffer(buff_radius) + except Exception as e: + log.debug("ToolExtrct.on_extrct_cutout_click() -> %s" % str(e)) + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("No cutout extracted."))) + return + + if not new_geo_solid.is_valid or new_geo_solid.is_empty: + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Failed."), _("No cutout extracted."))) + return + + new_apertures = { + '10': { + 'type': 'C', + 'size': thickness, + 'geometry': [ + { + 'solid': deepcopy(new_geo_solid), + 'follow': deepcopy(new_geo_follow) + } + ] + } + } + + def obj_init(new_obj, app_obj): + new_obj.multitool = False + new_obj.multigeo = False + new_obj.follow = False + new_obj.apertures = deepcopy(new_apertures) + new_obj.solid_geometry = [deepcopy(new_geo_solid)] + new_obj.follow_geometry = [deepcopy(new_geo_follow)] + + try: + new_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + except (AttributeError, TypeError): + pass + + with self.app.proc_container.new(_("Working ...")): + try: + self.app.app_obj.new_object("gerber", outname, obj_init) + except Exception as e: + log.error("Error on Extracted Cutout Gerber object creation: %s" % str(e)) + return + def on_hole_size_toggle(self, val): if val == "fixed": self.ui.fixed_label.setVisible(True) @@ -663,14 +746,14 @@ class ExtractUI: grid1.setColumnStretch(0, 0) grid1.setColumnStretch(1, 1) - grid1.addWidget(FCLabel(""), 0, 0, 1, 2) + # grid1.addWidget(FCLabel(""), 0, 0, 1, 2) self.extract_drills_label = FCLabel('%s' % _("Extract Drills").upper()) self.extract_drills_label.setToolTip( _("Extract an Excellon object from the Gerber pads.")) grid1.addWidget(self.extract_drills_label, 1, 0, 1, 2) - self.method_label = FCLabel('%s' % _("Method")) + self.method_label = FCLabel('%s:' % _("Method")) self.method_label.setToolTip( _("The method for processing pads. Can be:\n" "- Fixed Diameter -> all holes will have a set size\n" @@ -850,7 +933,7 @@ class ExtractUI: separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid3.addWidget(separator_line, 14, 0, 1, 2) - grid3.addWidget(FCLabel(""), 16, 0, 1, 2) + # grid3.addWidget(FCLabel(""), 16, 0, 1, 2) # EXTRACT SOLDERMASK self.extract_sm_label = FCLabel('%s' % _("Extract Soldermask").upper()) @@ -891,6 +974,59 @@ class ExtractUI: """) grid3.addWidget(self.e_sm_button, 24, 0, 1, 2) + # EXTRACT CUTOUT + self.extract_sm_label = FCLabel('%s' % _("Extract Cutout").upper()) + self.extract_sm_label.setToolTip( + _("Extract a cutout from a given Gerber file.")) + grid3.addWidget(self.extract_sm_label, 26, 0, 1, 2) + + # Margin + self.margin_cut_label = FCLabel('%s:' % _("Margin")) + self.margin_cut_label.setToolTip( + _("Margin over bounds. A positive value here\n" + "will make the cutout of the PCB further from\n" + "the actual PCB border") + ) + self.margin_cut_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.margin_cut_entry.set_range(-10000.0000, 10000.0000) + self.margin_cut_entry.set_precision(self.decimals) + self.margin_cut_entry.setSingleStep(0.1) + + grid3.addWidget(self.margin_cut_label, 28, 0) + grid3.addWidget(self.margin_cut_entry, 28, 1) + + # Thickness + self.thick_cut_label = FCLabel('%s:' % _("Thickness")) + self.thick_cut_label.setToolTip( + _("The thickness of the line that makes the cutout geometry.") + ) + self.thick_cut_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.thick_cut_entry.set_range(0.0000, 10000.0000) + self.thick_cut_entry.set_precision(self.decimals) + self.thick_cut_entry.setSingleStep(0.1) + + grid3.addWidget(self.thick_cut_label, 30, 0) + grid3.addWidget(self.thick_cut_entry, 30, 1) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid3.addWidget(separator_line, 32, 0, 1, 2) + + # Extract cutout from Gerber apertures flashes (pads) + self.e_cut_button = QtWidgets.QPushButton(_("Extract Cutout")) + self.e_cut_button.setIcon(QtGui.QIcon(self.app.resource_location + '/extract32.png')) + self.e_cut_button.setToolTip( + _("Extract soldermask from a given Gerber file.") + ) + self.e_cut_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + grid3.addWidget(self.e_cut_button, 34, 0, 1, 2) + self.layout.addStretch() # ## Reset Tool diff --git a/defaults.py b/defaults.py index a23dc942..8078bc26 100644 --- a/defaults.py +++ b/defaults.py @@ -713,6 +713,8 @@ class FlatCAMDefaults: "tools_extract_rectangular": False, "tools_extract_others": False, "tools_extract_sm_clearance": 0.1, + "tools_extract_cut_margin": 0.1, + "tools_extract_cut_thickness": 0.1, # Punch Gerber Tool "tools_punch_hole_type": 'exc',